Nginx is a web server that is designed on asynchronous, non-blocking, event-driven connection handling algorithm. As of this writing according to W3Techs serves around websites known to them. At Wego we use Nginx as our primary web server.
Recently Wego launched its mobile website as a : PWA, for significant visible speed and performance gains. This replaced our previous version of adaptive website. The challenge on the side for the launch was to keep serving the users under same domains i.e. over 50 domains for , and redirecting users to site for and the user to the current version.
Previously we have had a similar situation implementing a security system at our proxy level where we had to route our requests through a system sitting beside our proxy. For that we used Nginx statement in each block, and I can not reiterate enough on its evilness and the unexpected behaviour that it brings along its implementation, considering the size of our configurations, it was an excruciating task, since then we don't use the builtin anymore. In 's own word if is evil, so it is better to avoid their use.
So what is it that we use other than the available command set ? one solution was to have new end points for mobile user or redirect them from old landing pages to the new urls or solve the whole thing by redirecting them to a separate mobile (sub)domains, such practices are quite common where you see, or for new launched/demo products. We didn't want the user experience to be effected, for that the new PWA based mobile website was to be served seamlessly under the same domains and urls. The major advantage that this brought was the and campaigns that were currently in place , with significant amount invested to generate traffic they didn't go obsolete and were able to draw traffic to the new website.
Concluding on how if's are evil and that user experience takes precedence over all. The solution was to use a lesser known brilliance that supports i.e Lua. Not many know and not much is available on the internet either about how can be used to make respond to your requirements. The flavour of that has the lua-nginx-module built into it is Openresty, if you want you can compile and build with the module(s) as well. With Lua support you can assign variables, add logging, update/change request/response variables and select proxy_pass dynamically. All with the freedom and reliability of knowing it performs the way you expect it to. With added to our stack we continue to achieve more and solve new challenges.
Creating an application¶
Further we walk you through key programming practices that will give you a good start in writing Lua applications for Tarantool. For an adventure, this is a story of implementing… a real microservice based on Tarantool! We implement a backend for a simplified version of Pokémon Go, a location-based augmented reality game released in mid-2016. In this game, players use a mobile device’s GPS capability to locate, capture, battle and train virtual monsters called “pokémon”, who appear on the screen as if they were in the same real-world location as the player.
To stay within the walk-through format, let’s narrow the original gameplay as follows. We have a map with pokémon spawn locations. Next, we have multiple players who can send catch-a-pokémon requests to the server (which runs our Tarantool microservice). The server replies whether the pokémon is caught or not, increases the player’s pokémon counter if yes, and triggers the respawn-a-pokémon method that spawns a new pokémon at the same location in a while.
We leave client-side applications outside the scope of this story. Yet we promise a mini-demo in the end to simulate real users and give us some fun. :-)
First, what would be the best way to deliver our microservice?
Modules, rocks and applications¶
To make our game logic available to other developers and Lua applications, let’s put it into a Lua module.
A module (called “rock” in Lua) is an optional library which enhances Tarantool functionality. So, we can install our logic as a module in Tarantool and use it from any Tarantool application or module. Like applications, modules in Tarantool can be written in Lua (rocks), C or C++.
Modules are good for two things:
- easier code management (reuse, packaging, versioning), and
- hot code reload without restarting the Tarantool instance.
Technically, a module is a file with source code that exports its functions in an API. For example, here is a Lua module named that exports one function named :
To launch the function – from another module, from a Lua application, or from Tarantool itself, – we need to save this module as a file, then load this module with the directive and call the exported function.
For example, here’s a Lua application that uses function from module:
A thing to remember here is that the directive takes load paths to Lua modules from the variable. This is a semicolon-separated string, where a question mark is used to interpolate the module name. By default, this variable contains system-wide Lua paths and the working directory. But if we put our modules inside a specific folder (e.g. ), we need to add this folder to before any calls to :
For our microservice, a simple and convenient solution would be to put all methods in a Lua module (say ) and to write a Lua application (say ) that initializes the gaming environment and starts the game loop.
Now let’s get down to implementation details. In our game, we need three entities:
- map, which is an array of pokémons with coordinates of respawn locations; in this version of the game, let a location be a rectangle identified with two points, upper-left and lower-right;
- player, which has an ID, a name, and coordinates of the player’s location point;
- pokémon, which has the same fields as the player, plus a status (active/inactive, that is present on the map or not) and a catch probability (well, let’s give our pokémons a chance to escape :-) )
We’ll store these entities as tuples in Tarantool spaces. But to deliver our backend application as a microservice, the good practice would be to send/receive our data in the universal JSON format, thus using Tarantool as a document storage.
To store JSON data as tuples, we will apply a savvy practice which reduces data footprint and ensures all stored documents are valid. We will use Tarantool module avro-schema which checks the schema of a JSON document and converts it to a Tarantool tuple. The tuple will contain only field values, and thus take a lot less space than the original document. In avro-schema terms, converting JSON documents to tuples is “flattening”, and restoring the original documents is “unflattening”. The usage is quite straightforward:
- For each entity, we need to define a schema in Apache Avro schema syntax, where we list the entity’s fields with their names and Avro data types.
- At initialization, we call that creates objects in memory for all schema entities, and that generates flatten/unflatten methods for each entity.
- Further on, we just call flatten/unflatten methods for a respective entity on receiving/sending the entity’s data.
Here’s what our schema definitions for the player and pokémon entities look like:
And here’s how we create and compile our entities at initialization:
As for the map entity, it would be an overkill to introduce a schema for it, because we have only one map in the game, it has very few fields, and – which is most important – we use the map only inside our logic, never exposing it to external users.
Next, we need methods to implement the game logic. To simulate object-oriented programming in our Lua code, let’s store all Lua functions and shared variables in a single local variable (let’s name it as ). This will allow us to address functions or variables from within our module as or . Like this:
In OOP terms, we can now regard local variables inside as object fields, and local functions as object methods.
In this manual, Lua examples use local variables. Use global variables with caution, since the module’s users may be unaware of them.
To enable/disable the use of undeclared global variables in your Lua code, use Tarantool’s strict module.
So, our game module will have the following methods:
- to calculate whether the pokémon was caught (besides the coordinates of both the player and pokémon, this method will apply a probability factor, so not every pokémon within the player’s reach will be caught);
- to add missing pokémons to the map, say, every 60 seconds (we assume that a frightened pokémon runs away, so we remove a pokémon from the map on any catch attempt and add it back to the map in a while);
- to log information about caught pokémons (like “Player 1 caught pokémon A”);
- to initialize the game (it will create database spaces, create and compile avro schemas, and launch ).
Besides, it would be convenient to have methods for working with Tarantool storage. For example:
- to add a pokémon to the database, and
- to populate the map with all pokémons stored in Tarantool.
We’ll need these two methods primarily when initializing our game, but we can also call them later, for example to test our code.
Bootstrapping a database¶
Let’s discuss game initialization. In