Repository navigation
Tine's storage layer now has a formally proved model #702
Replies: 3 comments 3 replies
|
I love these kinds of posts. One question I have: Could the entire Logseq/Tine model be formalised? The aim would be to ensure that any future improvements proposed or implemented go through this validation process, so we can avoid the usual situation where we think adding something is the right thing to do, only to find a year later that the model is “broken”. |
|
OMG! Say "I'm not a software engineer" or "this is vibe coded" one more time! 🤣 Perhaps worth pointing out that the only reason you need all the conflict detection and resolution, is because there are external applications writing to the same graph, without coordinating with Tine. How does it handle failure in the middle of a rename that involves updating many other pages that contain references to the renamed page? does the rust backend acknowledge the whole thing at once or are the ack's more granular (per-operation)? do you use a WAL (write-ahead-log) or similar, such that if some of the writes succeeded and some failed, you could resume the pending ones during crash recovery? you mentioned the "lockstep conformance tests" on the code. Do these generate a large corpus of scenarios similar to how the model is tested? How does that corpus compare to the model tests?
is it useful to have this deliberate divergence? can we update the model instead?
This is the most confusing sentence out of the whole article. What are model variants? Why do we have multiple? I thought there was one model. Why are some variants deliberately broken? What are mutations to the save engine? (is this referring to running the lockstep conformance tests with additional code injected?) What's a mutation that survives? Is it an output or an input?
To be clear, when you mention the existing filesystems, these are only running in "normal/healthy" mode , right? Because to get them to generate faults, you'd need to mimic the underlying block device, AFAIK. When you say "real disks" here, I think you mean an artifical filesystem that you control to make it fail (which one do you use for this, btw?)
I don't know about document providers, but presumably network drives would just offer the same posix API as any other unix filesystem, and therefore wouldn't be all that different from a local filesystem, other than that they would respond a bit slower and may be more likely to have their own internal issues and/or fail io syscalls (which you already test)
what does this mean? straight, as opposed to what, indirectly? do you mean with some kind of sync/fsync option? do you mean that you run many syscalls concurrently and/or in parallel? (i suspect not, you would probably at most use a pool with a bounded size) |
Sure, but those are not conflicts. I was talking about conflicts due to multiple writers. Obviously you need crash recovery
aha! very interesting. thanks.
wow! Very cool. I've never seen mutation testing before.
aha. that makes sense. does this cover a wide range of possible syscall return values (and combinations thereof)?
I think you only need to test one of tmpfs/ext/btrfs; they all offer the same posix API and only differ in implementation details that are outside the scope of tine's testing (correct me if i'm wrong).
I don't have windows expertise to offer suggestions on the exact syscalls, but I will say (regardless of the OS):
|
Uh oh!
There was an error while loading. Please reload this page.
I wrote the outline of this post and asked Claude (Opus 5.5) to expand, polish, and fill in details. I'm sure Claude will be happy to answer any follow-up technical questions that I may not be able to answer; I'm trying to keep the post itself short-ish, but I think there is a lot of interesting stuff to be learned so ask away.
Storage is a key layer for Tine, for two reasons: it has a lot to do with making Tine fast, and it has to be solid, because Tine writes your real notes and every bug there is a potential data-loss incident.
As far as reports go, data safety has not been a big concern. But watching Claude work on storage fixes, I saw a pattern: make a fix, review it, find a race condition or another bug, fix that, find another race... Each fix took a long time to converge, and I could never be sure it had.
It was also never quite clear under what assumptions we were working, and what guarantees we wanted to get out of it. "Don't lose data" is not a specification.
So I decided we should have a written storage specification that is simple enough to reason about (at least for models), and that says clearly:
Fine: Claude wrote a spec, GPT (Sol) reviewed it and found bugs, Claude fixed them, Sol found more bugs... At some point I realized this process would probably not converge either. I remembered (partly from my training) that there is such a thing as model checking, and that those techniques might be relevant.
There was also a concrete reason this couldn't wait: the command line (
tineCLI; its first, read-only version is in 0.8 Beta on Linux and macOS). Until now, each editing window held its page's unsaved text and saved it itself, with the logic split between the frontend and the Rust backend. A CLI that edits pages while the app is open would have been one more independent writer racing the windows, exactly the kind of situation where the fix-review-find-another-race loop never ends. The new design has one owner per graph: a backend "page host" that holds every page's text and does all the file I/O. App windows and the CLI are both just clients that send it text plus the version it was based on. That is what already lets the CLI read your open, unsaved edits, and what will let its editing commands (next) write pages safely while you are typing in the app. But it moves the whole save path into a new place, so we wanted to know precisely what it guarantees before building the CLI on top of it.How it went
Week one (Sept 30 – Oct 3): spec first, then check it. Claude translated the English spec into Quint, an executable specification language built on TLA+, and checked it with random simulation and the TLC model checker. The checker kept finding executions where it was unclear whether anything had been "lost". Each one needed a ruling from me, which showed that the spec didn't actually define loss. We wrote that definition down; the next round still found six cases it didn't decide. After more than twenty rounds I stopped that approach.
Model first (Oct 3–4). We turned it around: the Quint model is the spec, and the English text only explains it. Two independent attempts (one by Claude, one by GPT) were merged into one model. This immediately found real design bugs that the English drafts also had. For example, a crash could make Tine forget that a save had not been confirmed, and a later discard of that page then lost the text on a power cut.
Exhaustive checking (Oct 4–8). TLC explores every possible interleaving of user actions, saves, crashes, power cuts and external writes, up to a size limit. This is compute-hungry: the state space explodes quickly, so the runs were spread over several machines for days. One complete run checked more than 4 billion steps between some 2 million distinct states, without a violation. Along the way it and the accompanying proofs found several more real defects, including typing during a discard and a launch step that trusted a file rename which might not survive a power cut. Final reviews of the model by two other models (GPT and Claude Fable) found two more, both fixed.
From "small graphs" to "every graph" (Oct 8–9). Model checking only covers bounded sizes. To go beyond that, the model was transcribed into the Lean proof assistant, and the whole guarantee was proved there in six rounds: for any number of pages, any number of edits, and every race scenario we model. Each round's proof was rebuilt and checked by an independent verifier agent. As a cross-check, the Lean and Quint versions were run side by side on every test scenario and agree on all 2,024 outcomes.
Rename and delete (Oct 9). Page rename (with rewriting every reference) and page delete were then added to the model. Lean proved that the extended model behaves exactly like the old one on everything that isn't a rename or delete. Its first attempt at the full guarantee failed, on a real hole that 120 hand-written scenarios and random simulation had all missed: you have unsent typing on a page, the command line deletes or renames that page, and your late typing silently overwrote the deletion. That case now comes back to you as a visible conflict. The fixed model was proved in full the same day.
Since then (Oct 10–14). The model kept growing as implementation needs came up: a fixed order for the steps of a rename (so that while Tine runs, the old page is only removed after the new page and every rewritten link are safely on disk), one general "operation" step that later features reuse, and a check that a new page can't take a name another file already claims. Each was added as an extension that leaves everything else unchanged, and each passes the full scenario set, every deliberately broken variant, and random simulation. The Lean proof caught up with them: the full no-loss guarantee, including renames and the new operation step, is proved for every reachable state, and so are the rename order, the name check and "deleted text always ends up in the trash". The proof work also found a spot where the model was sloppier than the code (cancelling a rename could reload a page that had already been closed); the model was corrected to match what the code already did. The last piece, that while Tine runs a renamed page always has a file at its old or its new path, is proved too (a crash or another program writing to one of those paths ends that promise; the no-loss guarantee still covers your text). Every property the model states is now proved for every graph size.
What is proved
Fast forward: we now have an English spec, a Quint model of it, and a formal proof that, under the stated assumptions, the current model gives these guarantees:
The proof is equally clear about what is not covered, and these are written down as accepted, not as losses:
As far as I know, this kind of work has so far been done mostly in large-scale industry: AWS has used TLA+ on its core services for over a decade, and Microsoft and MongoDB do similar things. Most of those check the model up to some size; here the guarantee is proved for graphs of any size. Academia goes further still: projects like FSCQ, a verified file system, prove that the code itself is correct, which we don't (see below). But I don't know of a hobby-scale app that does what we did.
And the code?
Then came the implementation. This was also not one-shot, and we don't have a mathematical proof that the Rust code conforms to the model. What we have instead:
So the chain is: English spec → Quint model (checked) → Lean proof (kernel-checked; the whole guarantee for the current model) → code (tested against the model, not proved). From 0.8.0-beta.2 on, the code is tested against the current model version, the one the proof covers (0.8.0-beta.1 was tested against the previous version, which lacks the later name-slot check and one guard fix). The two weakest links are named: the Lean copy matching the Quint model is checked by replaying scenarios rather than proved, and the code matching the model is tested rather than proved. The model also says nothing about how fast things are saved, and the guarantee is claimed for normal local filesystems; Android/iOS document providers and network drives run the same code on a best-effort basis.
This shipped in 0.8.0-beta.1 as the new background save engine. Does it cost performance? Mostly no, and one thing got faster. Compared with the Beta just before the new engine, on two real-sized graphs: the median time from a keystroke to the text being safely on disk dropped from about 1.3 s to about 0.5 s; launch and deleting a page are unchanged; renaming a page with many references is somewhat slower (about 15% on Linux). On Windows, renames that rewrite hundreds of pages are noticeably slower, because each rewritten file is now written straight through to disk. That trade is deliberate.
The spec, the Quint models, the scenarios and the Lean proofs are in the repository under
docs/storage-model/, with a README saying exactly what is proved for which model version.Why I'm excited
Time will tell whether this has really dealt with a big portion of storage bugs. But I am pretty excited about it: not only because it is good news for Tine's storage layer, but also because it shows promise for using similar techniques on other parts of Tine.
We cannot do the same thing everywhere. The UI, for example, is very complicated and it's not obvious how to formalize it this way. But the bar for this kind of rigor has dropped dramatically, with the models doing the heavy lifting. Model-checked hobby software may be becoming a reality.
All reactions