We at our techdev office in Berlin decided one day that the discussions about where to eat lunch had to come to an end. The lost time and increasing hunger while discussing were starting to get annoying. Andrea suggested to let a computer do the decision – and since we’re all programmers who can teach a computer to do this, why not? So a small C program (or app if you will) was born. It has a hard coded list of some places we like around Alexanderplatz where our office was located, chooses one and then emulates a deep thinking process.

#include <stdio.h>
#include <string.h>

int getrand(int max){
    srand ( time(NULL) );
    int r = rand();
    return r % max;
}

int main() {
    int number_of_places = 4;
    char orte[number_of_places][15];
    strcpy(orte[0], "Pizza");
    strcpy(orte[1], "Koreanisch");
    strcpy(orte[2], "Currywurst");
    strcpy(orte[3], "Salat");
    int zahl = getrand(number_of_places);
    printf("Today we are gonna eat at\n");
    sleep(1);
    int i;
    for(i = 0; i<5; i++){
        fflush(stdout);
        sleep(1);
        printf(".");
    }
    printf("%s\n", orte[zahl]);
    return 0;
}

Shortly after the tool was written we came to the conclusion that it needs to be run at least two times to get a satisfactory result for some reason.

The New Office

A few days ago we moved to our new office in Berlin near Eberswalder Strasse. There’s even more lunch options around here. Some of these options we know, others are new to us. The old C program didn’t do the trick anymore Who wants to recompile every time we add a restaurant? Also it was only installed on Markus’ computer, a classical single point of failure.

So we were in need for a new lunch decider. Since we’re into web development some sort of web app sounded like a good idea. We have a Raspberry Pi in the office that can run some kind of server. We also like to try out new things, so a stack we didn’t have experience with yet seemed preferable.

The requirements for the tool are simple:

  1. It must decide which lunch to get for us from a saved list of options
  2. New options should be easy to add

We have some future ideas (ratings, visit count, …) but as a start this will suffice.

OpenResty

I read about OpenResty a while ago on HackerNews. It’s basically Lua running inside of nginx with the option to access the request and response and do a lot of dynamic stuff with them. Now, I know neither Lua nor nginx really well – even less OpenResty. Everything I write here is what I gathered, learned and assumed in the few hours I used to hack our new lunch decider together. Feel free to correct me anywhere I’m wrong!

The idea of using nginx as an application server appealed to me and trying out Lua sounded like fun. I just had to add some frontend technology and a persistent storage to the mix and would be good to go. Since a Redis module is available for OpenResty (and I did not want to torture the Pi with Postgres) I chose Redis for persistence. The frontend should be written with knockout.js (and as a practice without jQuery for anything).

So let’s take a look at some code, shall we? You can find a link to the full application on our Github page at the end of this article!

Modelling Data in Redis

Redis is not a really appropriate solution as a storage for an app like this, nonetheless it’s fun to think of a way to store the data there. Here’s what I came up with:

  • lunch.options [1, 2, 4, 5, 6] These are the ids of the available lunch options. Since an option can be deleted there can be gaps.
  • lunch.options.id.sequence 6 This contains the currently highest id – a sequence to get the next id with INCR lunch.options.id.sequence.
  • lunch.options:$id name "Pizza" ... This is the actual option, a Redis hash so we can store more fields in the future.

Frontend

The frontend is a single page application written with just knockout.js. Let’s take a look at the part that gets a random lunch option from the backend!

<div id="lunch_decider" class="jumbotron">
  <h1>Where do we eat lunch today?</h1>
  <button class="btn btn-primary" data-bind="click: getLunchOption">Press to find out!</button>
  <h2>And the answer is:</h2>
  <p data-bind="text: lunchOption().name">...</p>
</div>
function LunchOptionModel(lunchOption) {
   var self = this;
   this.lunchOption = ko.observable(lunchOption);

   this.getLunchOption = function() {
       function randomOptionLoaded() {
          self.lunchOption(JSON.parse(this.responseText));
       }
       self.lunchOption({name: 'calculating...'});
       var request = new XMLHttpRequest();
       request.onload = randomOptionLoaded;
       request.open('get', '/random', true);
       request.send();
   }
}

ko.applyBindings(new LunchOptionModel({name: '...'}), document.getElementById('lunch_decider'));

Easy, we just need two knockout bindings (one click and one text), a single observable value in our model and the click handler. The most interesting part for me was to make the AJAX request – the last time I did that by hand I used the onreadystatechange event handler. The apparently newer onload` handler seems much easier though.

Lunch Decider Frontend

We also set the displayed value to “calculating..” since our backend will take a while to make that hard decision!

Backend

Now let’s take a look how that GET /random request from the frontend is handled in our OpenResty nginx server. First the routing: it is done with nginx location directives inside our server definition.

http {
  init_by_lua 'json = require("cjson");';
  server {
    listen 8080;
    # some other properties...
    location /random {
      default_type application/json;
      content_by_lua_file ./lua/lunch.lua;
    }
  }
}

We just tell nginx to call a Lua file containing a handler for the request. Caching of the Lua scripts can be turned off during development, giving it a PHP or JS like development cycle (just edit the script!).

The content of the Lua script (abbreviated):

local randomId = red:srandmember("lunch.options", 1);
local randomOption = red:hgetall("lunch.options:" .. randomId[1]);
local response = json.encode(red:array_to_hash(randomOption));

math.randomseed( os.time() );
local sleepTime = math.random(0, 2);
ngx.sleep(sleepTime);

ngx.say(response);

At the start I check the HTTP method and return 405 when it isn’t GET (just for fun) and initialize the Redis connection. If you want to see that boilerplate code please look at the repo.

We get a random member of the set of lunch option ids via the Redis method SRANDMEMBER. We then load all fields of the hash belonging to the id with HGETALL. Since the response of that is in the format ["name", "Pizza"] we convert it to a Lua table before converting it to JSON (with cJSON). We then simulate a thinking process of 0 to 2 seconds and return the response. The ngx object is the entry point to all nginx features in OpenResty. In a similar manner I can also return a list with all lunch options to display it to the user.

Bonus: Routing With Path Variables in OpenResty

I also wanted lunch options to be removeable. This means removing the id from the set in Redis and removing the key containing the option all together.

SREM lunch.options $id
DEL lunch.options:$id

I wanted to do this by a DELETE /options/$id HTTP request. But how to map the id in the route to a variable I can use in my Lua code? It’s actually not that hard.

I matched the route with a regex that captures the id inside a group. This group then can be set to a variable that is accessible in the Lua file with ngx.var.$varname.

location ~ ^/options/(\d+) {
    set $id $1;
    content_by_lua_file ./lua/single.lua;
}

This allows me to use ngx.var.id in single.lua and call the corresponding Redis commands!

Conclusion

Our new lunch decider was really fast to write, has more functions than the old one and runs happily from the Raspberry Pi. A small test with ab showed it is capable of around 70 requests/s at a concurrency level of 50 when getting the list of options via REST. That should suffice for our needs.

I really enjoyed mashing together a prototype like this in OpenResty. Since I was new to Lua I had to do some googling and the OpenResty documentation does not answer every question as easily as I would have liked but that did not really stop me. As far as I understood OpenResty uses event loop of nginx under its hood, so even a call like ngx.sleep(1) is asynchronuous but still blocks!

You can find the code for the application here. Feel free to use it for your company or submit a pull request!