Field report on upgrading a rocket v0.4 application to v0.5
The application in question is my first foray into the rust language and a very simple, CRUD-style web application. It consists of:
- a page with list of entries in a database, sorted by fixed criteria, for now
- a single JSON API endpoint to retrieve that same list, with optional filters
- a form to add an entry to the list
- a nearly static about page
- two tasks started via cron jobs, one running every few minutes and the other once a day
The rocket framework appealed to me because it offered a similar semantic to flask for python. It comes with several options for templating and state (aka database) integrations. I had picked tera templates, as they are near identical to jinja2 templates that are used all over python application (i.e. in pelican, ansible, flask, ...) and diesel for accessing an sqlite database. The rocket framework was already at version 0.4 when I started out with the project in 2020. It used a synchronous version of hyper, a low-level HTTP library to provide a multi-threaded HTTP server. It lets you compile the entire application into a single statically linked binary, excellent to run in a small "FROM scratch" container image.
In June 2021, the rocket 0.5 release candidate got published, and over the quieter days end of 2021 / beginning of 2022, I dug into upgrading the app. Let's start with the best bits about this process and the end result:
- I didn't need to do any changes to the templates, the CSS, the database structures or queries. The rocket routing mechanism worked the same way as before.
- rocket 0.5 is now compiling on a stable rust compiler, where 0.4 required the use of the nightly compiler (meaning it used some unstable language features). It got rewritten to use asynchronous functions, which allows it to use the latest hyper library which leverages that mechanism on top of a multi-threaded runtime provided by tokio, an implementation of the green threads concept in Rust.
For this particular application, the average latency was reduced from 67ms to 60ms (as monitored by a zabbix proxy running an a host attached to the same subnet) and the daily cronjob duration got reduced from 52s, using multi-threading, to 22s, using async and the tokio runtime (the earliest, fully linear, implementation of that task took 20 minutes).
While I am happy with these clear improvements, they did come at some cost:
- In the end I spent about 7 workdays on this upgrade, spread over 2 weekends and several evenings - git tells me that this consisted of 6188 additions and 4231 deletions, including all the license changes.
- The switch to async/await in both rocket and hyper required that many formerly synchronous functions had to be made async as well, as they now needed to call async functions themselves. Rust would offer you the block_on construct to call async functions from sync ones, but that conflicts with the tokio runtime and causes it to fail. The same is true for spawning threads, which I had done extensively in parts of the code that run multiple HTTP(S) requests in parallel. Using tokio to let the async calls be handled automatically did pay off and is indeed faster then my manual optimizations, but the rewrite into async required pretty big code changes.
- The newer hyper version lost some features I had been relying on, like shorter connection timeouts (I replaced these with a timeout wrapper on the returned future and an ugly workaround to return an error) and IDN domain names (I now use the URL parser from servo, developed for Firefox).
- The unit tests also needed to be made async aware and tokio provides a dedicated macro for wrapping tests. Unfortunately I had to give up on one unit test that I just couldn't get to work under a tokio runtime, while the error couldn't be reproduced on the running service, either manually or scripted. It seems the tokio runtime acts differently when in a unit test, where it keeps telling me it can't launch a runtime from within a runtime. The stack traces seem to go remain outside any of my code, so doesn't even seem to enter any part of the unit test itself.
- Due to all these new and upgraded libraries the statically linked binary ended up growing (after stripping and upx-ing) from 2.16 MiB to 2.83 MiB. Sure, compared to a full PHP- or Python-stack it is still at least an order of magnitude smaller, but it's also an increase of 1/3.
I would still recommend rocket to anyone familiar with flask. Creating dynamic web pages or APIs is very straight forward to do. The issues I encountered with the switch to async would not apply to new projects anyway. For existing rocket v0.4 applications, the upgrade is worthwhile if you can use the extra performance or aren't doing weird multi-threaded things in the background, like me. None of the core rocket features broke, only the database abstraction required modest changes. My struggle was mostly with the use of hyper as a client.
For the database side, diesel isn't quite as elegant as ORMs in other languages. It makes it simple to retrieve (lists of) elements from a table and do simple joins. For more complex things you can fall back to writing manual SQL queries and mapping the result onto your structs for convenience (you can mark properties that aren't tied to specific table columns, so you can populate them from calculations on joined tables or such). The database migration mechanism also requires the use of plain SQL.
Review: PediaPress - Wikipedia für Internetausdrucker
In Vorbereitung auf einen Urlaub in Ägypten wollte ich mir Wikipedia-Artikel über die Geschichte des Landes und geplanter Ausflugsziele organisieren. Meine ursprüngliche Idee war die Artikel in epub-Dateien zu konvertieren um diese "Hitchhikers Guide"-mässig als Enzyklopädie auf meinem Ebook-Reader immer dabei zu haben.
Leider ist es mit nicht wenig Nachbearbeitung verbunden aus einer heruntergeladenen Webseite oder mit copy-paste und einer entsprechenden epub-Software (z.B. Sigil oder Calibre) eine Datei anzufertigen, die auch einigermassen benutzbar ist, von Fotos und Graphiken gar nicht zu sprechen.
Nun bietet Wikipedia seit einiger Zeit einen Buchgenerator, mit dem man "ein Buch aus beliebigen Wikiseiten erstellen [... und ...] in unterschiedlichen Formaten exportieren (beispielsweise PDF oder ODF)" können soll. Ich hatte sogar Berichte gefunden, dass es damit möglich sei auch epub-Dateien zu erzeugen. Sobald man auf der Buchgenerator-Seite die Funktion aktiviert hat, kann man beliebige Artikel per Mausklick markieren und kann auf eine Übersichtsseite wechseln. Dort kann man die Reihenfolge der Artikel per drag-and-drop anpassen und Kapitel einführen. Ist man fertig, kann man den Export starten.
Leider stand zum Zeitpunkt als ich die Funktion nutzen wollte (ca. 22. Februar 2015) nur noch PDF als Export zur Auswahl. Und der ist bei mir stets mit einer Fehlermeldung gescheitert. Man kann sich aber auch über einen kostenpflichtigen Dienst der Firma PediaPress GmbH in Deutschland das zusammengestellte Buch ausdrucken lassen.
Auch wenn ich mich durch die fehlerhafte Exportfunktion ein wenig genötigt fühlte, habe ich das dann doch ausprobiert und schliesslich bestellt. Auf der PediaPress-Webseite kann man seinem Buch einen Titel und Untertitel geben, einen "Editor"-Namen angeben und eine Widmung verfassen. Auch kann man zwischen etwa einem dutzend verschiedenen Cover-Farben wählen und eines der Bilder aus den gewählten Artikeln als Coverbild auswählen.
Der Preis wird anhand der Anzahl Seiten berechnet. Wählt man zudem die Optionen Hardcover oder Farbdruck, verdoppelt, respektive verdreifacht, sich der Preis.
Mein Ägypten-Buch ist letzte Woche eingetroffen und ich bin mit der Qualität sehr zufrieden:
Wie man sieht habe ich mir die Optionen Hardcover und Farbe geleistet. Die Druckqualität ist absolut mit der vom "Grossverlag" vergleichbar. Ich habe da schon sehr schlecht gerasterte Druckbilder bei im "Selbstverlag" oder über Amazon Print on Demand erschienenen Büchern gesehen. Tabellen werden ebenfalls sauber übernommen.
Die Fotos sind wenn möglich auf Seitenbreite oder etwas stärker verkleinert und daher meist in sehr guter Qualität. Einzelne Bilder sind pixelig, weil deren Ursprungsgraphiken nur eine sehr kleine Auflösung haben.
Der automatische Textsatz erledigt einen sehr guten Job: Pro Seite hat es nie mehr als zwei Bilder. Die Bilder werden teilweise etwas verschoben um am Anfang einer Seite zu stehen. Selten findet man drei Bilder nacheinander, welche dann auf eineinhalb Seiten verteilt werden. Einige kleinere Bilder werden im Text zwischen zwei Absätzen dargestellt.
Die Fussnoten aller Artikel werden am Ende des Buches in ein Gesamtfussnoten-Verzeichnis zusammengefasst. Link-Unterstreichungen gibt es keine, die würden in einem gedruckten Buch ja auch keinen Sinn machen. Ebenfalls am Ende des Buches ist eine Quellen- und Autorenliste zu allen Artikeln zu finden. Für alle Bilder gibt es eine separate Quellen-, Autoren und Lizenzliste. Es folgt noch ein Abdruck der Creative Commons und GNU Free Documentations Lizenz und ein Schlagwort-Index.
Seine Grenzen erreicht der Textsatz bei gemischten Arabischen und lateinischen Texten. Solange das Arabische auf einem eigenen Absatz steht funktioniert es gut. Steht das arabische Wort jedoch innerhalb eines Absatzes aus lateinischen Buchstaben, scheint es bei den Zeichen vor dem Wort zu Zeichen-Verdrehungen zu kommen. Vermutlich hat das System Mühe mit den Wechsel von links-nach-rechts auf rechts-nach-links und wieder zurück (Arabisch wird wie Hebräisch von rechts nach links geschrieben).
Gänzlich unterschlagen hat der Textsatz die ägyptischen Hieroglyphen die in den meisten Artikel-Köpfen der Sehenswürdigkeiten vorkommen. Diese werden auf der Wikipedia mit Tabellen und kleinen Graphiken zusammengetrickst. Vor solchen inline-Tabellen hat die Software wohl lieber kapituliert als etwas unleserliches zu produzieren.
Was mir beim Zusammenstellen etwas Bauchweh bereitet hat, war die Editor/Herausgeber-Beschriftung, welche ich schliesslich mit "Wikipedia" befüllt habe. Vom Formular-Beschrieb her und der Darstellung auf dem Buch ist diese deutlich als "Autor" zu verstehen. Da ich die Artikel ja nicht geschrieben, sondern bloss ausgewählt habe mochte ich da nicht meinen Namen stehen haben. Wie ich anhand des PediaPress-Katalogs sehe, bin ich nicht der einzige der dort etwas anderes als seinen Namen angegeben hat.
Zusammengefasst lässt sich zum Service sagen:
Pro:
- Sehr gute Druck-Qualität
- Preis je nach Budget und Bedarf anpassbar
- Lieferung innert 2 Wochen
- Korrekte Lizenzierung innerhalb des Buches
Contra:
- Wenn man mit dieser Funktion elektronische Dokumente erstellen könnte, würde sich der Druck erübrigen
- Schnell ist ein umfangreiches Buch zusammengeklickt, entsprechend hoch ist dessen gedrucktes Gewicht
- Die Urheberschaft des Buches kann bei oberflächlicher Betrachtung irreführend sein
Auf jeden Fall freue ich mich schon auf die Reise und bin gespannt wie nützlich mir dieses "massgeschneiderte" Buch sein wird.
elrido
Als Antwort auf elrido • •As a brief follow up, I did eventually manage to fix that last unit test. It was a pretty large end-to-end affair, where I used both rocket's Client, to send form data, and also triggered the cron jobs that use their own rocket instances, but only to extract the database connection. This seems to have been the cause of the tokio within tokio runtime issue. I simply split this into two tests, one that used the form and the other to test all the cron job functions.
The binary acts as a multi-call one, when started with the regular rocket environment parameters it will launch the web service. But if the CRON environment variable is detected it will only execute the requested cron mode and exit. This is not a normal use case for a rocket application, so I can only really blame myself for this quirk.
One thing I did also find out is that my test code did have side effects, because most tests use a shared database (sqlite), so I had to switch to running all tests single-threaded (which is slower, takes up to 10s) as otherwise I got random failures when some tests deleted oth
... mehr anzeigenAs a brief follow up, I did eventually manage to fix that last unit test. It was a pretty large end-to-end affair, where I used both rocket's Client, to send form data, and also triggered the cron jobs that use their own rocket instances, but only to extract the database connection. This seems to have been the cause of the tokio within tokio runtime issue. I simply split this into two tests, one that used the form and the other to test all the cron job functions.
The binary acts as a multi-call one, when started with the regular rocket environment parameters it will launch the web service. But if the CRON environment variable is detected it will only execute the requested cron mode and exit. This is not a normal use case for a rocket application, so I can only really blame myself for this quirk.
One thing I did also find out is that my test code did have side effects, because most tests use a shared database (sqlite), so I had to switch to running all tests single-threaded (which is slower, takes up to 10s) as otherwise I got random failures when some tests deleted others data or couldn't add new instances when these were already present. This could obviously be fixed by either providing each test with a mock database or just be more careful designing the tests such that their order doesn't matter (i.e. using unique keys to add to the database for each test.
Boring details start around this point in the commit.