My adventures in Rust webdev

People who know me in the Rust community usually think that I’m a game developer, since I contribute mostly to game development libraries in Rust. What they probably don’t know is that in real life I’m doing backend web development for a living, while gamedev is only my dream job.

One of the services I’m in charge of is an internal service that was written in node+express in their early years (for various reasons that are off-topic here). As it is private it is running on a single server and doesn’t get a lot of traffic. Over time, because of several large changes in the requirements, the code has started being crippled with quirks and bugs. Around mid-2015, I started rewriting it in Rust.

I pushed the rewrite in production around the end of 2015, and am now in the process of rewriting another similar code in Rust. Here is a short “post-mortem” of what worked/works well and what was/is troublesome.

The framework

The most popular Rust web frameworks today are Iron and Nickel, but there exist several other personal web frameworks with a few github stars each. As far as I can tell, all the frameworks that I found in my quick overview are express-like middlewares-based frameworks.

While the advantage of using middlewares is that people coming from dynamic languages are going to be more or less familiar with them, I think that this is really an unidiomatic design in Rust. You have to put things in static objects that are referred by their name and handle lifetime issues, while in my opinion it’s not better than simply using regular variables.

For this reason I decided to write my own framework named rouille, which I’m very satisfied of. I don’t want to enter “the frameworks game” because I try to focus my open source contributions on gamedev, but I would definitely recommend people to use rouille’s simple “flat” design compared to middlewares.

Templates

Since the old website was using HTML templates written in mustache, I decided to keep that system and use the mustache Rust library.

There’s nothing to say on the Rust library itself, it works great, but there are things to say about how to use it and mustache itself.

Caching

First of all, I had the idea to use a caching mechanism for the templates. The first time I use a template, it would compile it and store it in memory. Then when you reuse the same template, it would load the already-compiled template from the cache. The cache itself was stored in a lazy_static!.

This seemed like a good idea but I quickly realized that it’s a nightmare for development, as I had to restart the server every time I made a change to a template. This is especially annoying because for an undetermined reason the server remains inaccessible for about ten seconds after it is supposed to have started listening.

Another problem of that caching system is that if building a template would panic (because the template is malformed), the mutex that locks the cache would become poisoned. All the routes of the server that use any template would then automatically panic as well because of that. This problem was easily bypassed by rewriting some parts of the code in a cleverer way, but it’s definitely a problem that I wasn’t expecting.

I ended up removing this caching mechanism entirely. It would be nice to enable it when compiling in release mode and disable it in debug mode, but there’s apparently no clean way in Rust to do that.

Usability

Just like the middlewares system that I talked about earlier, the mustache templating system was designed for weakly-typed languages. For example when you write {{#foo}}, the value of foo passed from Rust can be an Option, a struct, a Vec, a boolean, a function, and so on.

Contrary to node for example where you would just pass { foo: bar } to the template engine, in Rust you have to manually create a struct that contains the foo member with its type, and then instantiate that struct. Several hundred lines of my code are dedicated purely to struct definitions that would be unnecessary in a weakly-typed language. That doesn’t mean that weak typing is better for templating, it just means that mustache is not really adapted to Rust.

Usually the major selling point of strong typing is that it provides better guarantees. But in the case of mustache, you don’t get any compilation error if you make a typo in an identifier or in the template. I usually tell people that the advantage of Rust is that your code will just work almost all the time once you managed to compile it, but with mustache it is clearly not the case with typos not even being detected.

This area of Rust web development could definitely get some improvements. As an experiment I quickly wrote a small library that compiles templates at compile-time and generate Rust structs at compile-time, but I found out that in some situations it is also very unpractical. It could be an area that could be investigated more.

Postgres

I’m using the postgres library in order to connect to the database. It works great, if you except… OpenSSL.

OpenSSL

Everyone who has tried web development in Rust for some time probably knows what I mean. This library is a nightmare. It is for example the reason why I’m using a Linux virtual machine to develop on my Windows machine, instead of compiling for Windows directly.

Outside of linking issues, the biggest problem is the fact that there are multiple versions of openssl-sys, and they all share the same links attribute used by Cargo to determine whether two libraries can be used at the same time.

This means that for example if you want to use at the same time postgres 0.9 (which indirectly depends on openssl-sys 0.6) and curl 0.3 (which depends on openssl-sys 0.7), you will get a compilation error from Cargo because openssl-sys 0.6 and 0.7 can’t be used at the same time.

This problem hit me three times in total, forcing me to update all my dependencies. One of these updates took me one and a half hours, just because of that “administrative” and non-technical problem.

While writing this article I discovered that recent versions of postgres accept multiple versions of openssl, which is a good thing. However I feel like it’s still an unresolved problem and that the design of the links attribute needs a change.

Postgres‘s usability

The postgres library itself can be a little tedious to use. I started the project by writing code that would manually read columns from result sets. This makes the code really tedious to write and difficult to read.

Therefore in order to make my life easier, I wrote some macros which you can find here. I strongly encourage you to use these, or to write similar macros. The only operation that doesn’t have a macro is executing a query that doesn’t return anything (such as an UPDATE), for which I just call execute.

Despite these macros, everything I said about mustache’s usability also applies to SQL queries. It would be so much nicer to check the SQL code’s validity or even the data types at compile-time in order to avoid risking an error 500 if you make a mistake and forgot to write a test for something. Again, procedural macros could come to our rescue.

Chrono

One thing I didn’t mention is that the code manipulates a lot of dates and hours. It is in fact from this area that most of the bugs in the old code came from, as handling date/times in Javascript is a total clusterfuck.

I just want to say that the chrono crate is absolutely pure happiness compared to Javascript. Just for this alone, I think it was worth rewriting in Rust.

Deployment

Every popular language has its utilities and tutorials about how to properly deploy a web service written in that language. But Rust doesn’t.

Right now my design is very archaic. Whenever I want to deploy an update, I have to connect with SSH to the production server and run a shell script. This script pulls the latest version of the code from a git repository, then builds in release mode the library part of the package (which takes a good five minutes, as the server is not very powerful), then stops the current daemon, builds the main executable in release mode (which takes a few seconds), then starts that executable as a daemon.

This downtime of a few seconds is okay-ish because it is an internal utility, and because I try to deploy updates outside of work hours. But I would not want to do that for a site getting public traffic.

The fact that the production server builds the Rust executable also caused a problem which I hadn’t anticipated: the compilation process uses almost all the memory (1 GB) of the server. Recently I actually hit that limit, and the OOM killer of the server silently killed the MySQL daemon that was running on it. Not cool, and stressful when I got emailed about everything being broken ten minutes later. I really wouldn’t want that to happen on a “real” production server with thousands of clients.

After this event, I tried the solution of building the main executable during the continuous integration and sending it to the production server. In addition to the increased security risk of executing something sent over the Internet without any check, I encountered a problem which is that the built executable couldn’t find OpenSSL’s shared libraries on the production server (yes, OpenSSL again). I abandoned this idea.

Right now my idea is to build a small executable that would act as an HTTP proxy around the real website. Whenever an HTTP request is made to a special route, the proxy would invoke git to pull the latest version then Cargo to compile the real website. This would solve the downtime problem and would make the deployments automatic, but it wouldn’t solve the out-of-memory error. I haven’t written that code yet, but will probably do so in the next few weeks.

Overall

Overall I’m quite satisfied of Rust. I still wouldn’t recommend web development in Rust for everyone, unless it’s really worth it or if you like to figure out things yourself. But it’s (very) slowly getting there.

The server didn’t crash once since I have started it for the first time nine months ago, which is very satisfying. I got approximately half a dozen different panics (that are caught and translated into error 500s), all of them trying to reading a NULL value from the database in a non-Option variable.

In fact the biggest worry I still have about Rust is the fact that it’s a niche technology for webdev (note: I wouldn’t consider Rust niche anymore for system programming, but it is very niche for webdev). I was lucky enough to be able to choose the language I want for this project, but if I ever leave this client it will be hard for them to make deep changes to the code. But if everybody made the same reasoning, nothing would ever get started.