Improving how Mollymawk handles data
2026-10-14Mollymawk from the beginning has been running on it's MVP version. This version mainly focused on getting things to work which was often crude, and not much about how efficient it was. At this point of the journey, we are now focusing on polishing mollymawk up a bit more, and finetuning the underlying implementations we have done.
To put this into context, some issues with the MVP were:
-
how much data mollymawk was storing, as well as how mollymawk was storing it. Simply put, all stateful data, whether they were user information, configuration data, or short-lived data such as csrf tokens and cookies were dumped onto the storage device as a single json blob. This meant each time a user accesses any endpoint, be it a GET request, or a POST request (i.e mollymawk just loading an image on the landing page), led to a full disk rewrite, as cookies get updated with each request. Furthermore, all this data internally was being handled using Lists - each time we find a user, or try to authenticate a token or cookie meant traversing the whole list (and it's sub-list). On the performance side, this was a nightmare.
-
type safety - names were strings (which is fine), but also things like uuid values, time values and email.
-
given the initial storage design of mollymawk was to glue all data into one big blob of json and dump it to disk, we used a filesystem OneFFS. On the README page, OneFFS is defined as:
One-file filesystem. This is a filesystem for storing a single unnamed file. It is suitable for storing data that may be lost on updates. For example a cache of pre-computed cryptographic checksums that are slow to compute, but that we would rather recompute than have stale checksums.
It's worth mentioning that by design, OneFFS is not meant to be fault-safe and this is why we also had to rethink how mollymawk persists data.
In this article, we'll walk through how we migrated mollymawk's data structures from lists to maps, introduced strict type safety, decoupled ephemeral data from disk persistence, and moved our block storage layer to a more fault-safe filesystem (including migrating previous data to the new filesytem).
Changes made
Type safety
All data which was previously "stringly-typed" has been migrated to a more appropriate type safe version:
- UUID values:
Uuid.tfrom uuidm. - Time values:
Ptime.tfrom ptime. - Email values:
Mrmime.Mailbox.tfrom mrmime
Other values which were correctly strings, such as names, but that need to be in comformity with albatross - we also changed them to directly use what albatross uses internally, which is Vmm_core.Name.Label.t, and now we can be sure that constructing values of this type in mollymawk will have validation within albatross.
Migrating from Lists to Maps
To improve the performance of mollymawk, we replaced the various lists with maps using OCaml's Map functor.
module LM = Map.Make (Vmm_core.Name.Label)
module SM = Map.Make (String)
module UM = Map.Make (Uuidm)
So Map.Make takes a module, such as Uuidm and creates a map where the key is of type Uuidm.t. Same applies for the others.
Then we migrated each sub-lists into a map - which are:
- Albatross configurations (system wide in mollymawk)
- Scaling policies (within a users data, keeps settings about how to scale their unikernels like how many instances can be created during cloning)
- Unikernel updates (also within a users data, keeps information about the unikernels the user has updated so we can have rollback functionality)
- API tokens (used to store a user's tokens which is used to authenticate requests made to mollymawk's API endpoints)
- Cookies (so we can know which user sessions are active)
Then we created secondary maps (indexes), which are not persisted to disk but help for quick lookups:
by_uuid- to search a user by uuidby_email- to search a user by email (such as during login)by_username- same as the names used internally by albatross, this is useful when we are doing operations for the users resources that are handled by albatross.by_token- for api calls
With secondary indexing in place, API token authentication no longer needs to scan every user in the database. In terms of complexity, if we wanted to authenticate that a token indeed belonged to a user, we will have to traverse the users list and for each user, scan all their tokens. Now with maps, the lookup happens once in the secondary index, and then in the primary users map.
With 10,000 users holding 5 tokens each (T = 50,000), the list version can do up to 50,000 comparisons. The map version does roughly log₂(50,000) + log₂(10,000), which is 16 + 14 = 30 comparisons, which is a great performance improvement.
Data persistency
With the upates so far, the next logical step was to stop persisting data which is meant to be short-lived, such as cookies and csrf tokens. As mentioned above, every time a user refreshed a page or submitted a form:
- A new CSRF token was generated.
- The user's
cookieswere updated. - The entire storage state was serialized to JSON.
- The JSON blob was written to the block device.
To plainly put it, everytime a user accessed a page, the entire json blob was written to disk. Now, cookies and csrf tokens are no longer persisted but are kept in memory as ephemeral data. A result of this is:
- One-time CSRF verification: When a form is submitted, mollymawk validates that the token belongs to the authenticated user, verifies that it was generated within the 1-hour expiry window, and immediately unregisters it to prevent replay attacks.
- Automatic pruning: Expired sessions and old CSRF tokens are pruned automatically.
- Zero disk I/O on web requests: Browsing pages, generating CSRF tokens, logging in, and verifying forms now occur strictly in memory. Disk writes only occur when a user explicitly alters persistent data (e.g. changing passwords, updating scaling policies, or generating API tokens).
On-disk storage fault-safety.
We have moved mollymawk's storage from oneffs to a custom filesystem. This works well and is already used by another project of ours tlstunnel, which is also a unikernel. This custom filesystem has one main advantage, fault safety - by which at any point of program execution, the program may stop, but the persistent data is still in a readable state (either the data before is intact or the new data is live). Some details about how this is achieved is
- dual redundant superblocks: superblocks written at both the first sector and the last sector of the block device which contains SHA-256 checksums of both metadata and data.
- self-healing on boot: when mounting the block device, both superblocks are read and if one has an invalid checksum or an older generation counter, the filesystem boots from the healthy superblock and automatically repairs the corrupted one.
- atomic commits: the superblocks are only updated once all data sectors have been committed, ensuring crash consistency.
Migrating data from oneffs to the new file system
To be sure we can safely migrate all our data to the new filesytem, we wrote a binary executable which takes a source and destination disk, connects to the source disk with oneffs, reads the data into memory, connects to the destination disk, formats it with the new filesystem and then writes the data to it.
Conclusion
In this article, we explored how we updated mollymawk's internal data structures, improved type safety, changed the filesystem used for persistency, and a way to migrate data from the old block device to a new one. There's still some things to be done about making mollymawk more efficient - such as moving away from mirage to a new stack (mkernel, mnet and vif) - but we are in the right direction.
This refactoring was carried out across the following pull requests:
- Data Structure Migrations:
- Cookies migrated from list to map: https://git.robur.coop/robur/mollymawk/pulls/20
- API tokens migrated from list to map: https://git.robur.coop/robur/mollymawk/pulls/21
- Unikernel updates migrated from list to map: https://git.robur.coop/robur/mollymawk/pulls/22
- Scaling policies migrated from list to map: https://git.robur.coop/robur/mollymawk/pulls/23
- Users migrated to maps with secondary indexes: https://git.robur.coop/robur/mollymawk/pulls/26
- Configurations migrated from list to map: https://git.robur.coop/robur/mollymawk/pulls/27
- Type Safety & Cleanup:
- User records and updates use Uuidm.t: https://git.robur.coop/robur/mollymawk/pulls/28
- Remove redundant user UUIDs from cookies: https://git.robur.coop/robur/mollymawk/pulls/29
- Ephemeral Sessions:
- Ephemeral sessions and CSRF tokens in memory: https://git.robur.coop/robur/mollymawk/pulls/30
- Migrate from OneFFS: https://git.robur.coop/robur/mollymawk/pulls/31