Comment by jorangreef

1 year ago

Thanks!

Yes, we have around 6,000+ assertions in TigerBeetle. A few of these were overtight, hence some of the crashes. But those were the assertions doing their job, alerting us that we needed to adjust our mental model, which we did.

Otherwise, apart from a small correctness bug in an internal testing feature we added (only in our Java client and only for Jepsen to facilitate the audit) there was only one correctness bug found by Jepsen, and it didn’t affect durability. We’ve written about it here: https://tigerbeetle.com/blog/2025-06-06-fuzzer-blind-spots-m...

Finally, to be fair, TigerBeetle can (and is tested) to survive more faults than Postgres can, since it was designed with an explicit storage fault model and using research that was not available at the time when Postgres was released in ‘96. TB’s fault models are further tested with Deterministic Simulation Testing and we use techniques such as static memory allocation following NASA’s Power of Ten Rules for Safety-Critical Code. There are known scenarios in the literature that will cause Postgres to lose data, which TigerBeetle can detect and recover from.

For more on this, see the section in Kyle’s report on helical fault injection (most Raft and Paxos implementations were not designed to survive this) as well as a talk we gave at QCon London: https://m.youtube.com/watch?v=_jfOk4L7CiY

Hi Joran,

I have followed TigerBeetle with interest for a while, and thank you for your inspirational work and informative presentations.

However, you have stated in several occasions that the lack of memory safety in Zig is not a concern since you don't dynamically allocate memory post startup. However, one of the defects uncovered here (#2435) was caused by dereferencing an uninitialized pointer. I find this pretty concerning, so I wonder if there is something that you will be doing differently to eliminate all similar bugs going forward?

  • TigerBeetle uses ReleaseSafe optimization mode, which means that the pointer was in fact initialized to 0xaaaaaaaaaaaaaaaa. Since nothing is mapped to this address, it reliably causes a segfault. This is equivalent to an assertion failure.

  • Note that that's a bug in the client, in the Zig-java FFI code, which is inherently unsafe. We'd likely made an a similar bug in Rust.

    Which is, yeah, one of the bigger technical challenges for us --- we ship language-native libraries for Go,node,Java,C#,Python and Rust, and, like in the Tolstoi novel, each one is peculiar in its own way. What's worse, they aren't directly covered by our deterministic simulator. That's one of the major reasons why we invest in full-system simulation with jepsen, antithesis and vortex (https://tigerbeetle.com/blog/2025-02-13-a-descent-into-the-v...). We are also toying with the idea of generating _more_ of that code, so there's less room for human error. Maybe one day we'll even do fully native client (eg, pure Java, pure Go), but we are not there yet.

    One super-specific in-progress thing is that, at the moment, the _bulk_ of the client testing is duplicated per client, and also the _bulk_ of the testing is example-based. Building simulator/workload is a lot of work, and duplicating it for each client is unreasonable. What we want to do here is to use multi-process architecture, where there's a single Zig process that generates the workloads and generates interesting sequences of commands for clients, and than in each client we implement just a tiny "interpreter" for workload language, getting a test suite for free. This is still WIP though!

    Regarding the broader memory safety issue in the database. We did have a couple of memory safety bugs, which were caught early in testing. We did have one very bad aliasing bug, which would have been totally prevented by Rust, which slipped through the bulk of our testing and into the release (it was caught in testing _after_ it was introduced): https://github.com/tigerbeetle/tigerbeetle/pull/2774. Notably, while the bug was bad enough to completely mess up our internal data structure, it was immediately caught by an assert down the line, and downgraded from correctness issues to a small availability issues (just restarting the replica would fix it). Curiously, the root cause for that bug was that we over-complicated our code. Long before the actual bug we felt uneasy about the data structure in question, and thought about refactoring it away (which refactor is underway. Hilariously, it looks that just "removing" the thing without any other code changes improves performance!).

    So, on balance, yeah, Rust would've prevented a small number of easy bugs, and on gnarly bug, but then the entire thing would have to look completely different, as the architecture of TigerBeetle not at all Rust-friendly. I'd be curious to see someone replicating single-thread io-uring no malloc after startup architecture in Rust! I personally don't know off the top of my head whether that would work or not.

    • I remember reading a similar thing about FoundationDB with their DST a while back. Over time, they surfaced relatively few bugs in the core server, but found a bunch in the client libraries because the clients were more complicated and were not run under their DST.

      Anyways, really interesting report and project. I also like your youtube show - keep up the great work! :)

The correctness bug was due to combinations of features. I'm curious if you've looked into combinatorial testing which NIST claimed knocks out almost all bugs when 6-way testing was used.

https://csrc.nist.gov/projects/automated-combinatorial-testi...

My intro to other categories of test generation was usually this paper:

https://cs.stanford.edu/people/saswat/research/ASTJSS.pdf

Maybe see of your team can build combinatorial- or path-based testing in Zig next.

> we have around 6,000+ assertions in TigerBeetle.

Are they enabled in production? Are there some expensive ones that aren’t?

  • Yes, we drive with the seat belts on.

    It’s not expensive.

    Because we batch, this naturally separates the control plane from the data plane, amortizing assertions against the (larger) buffers now flowing through the data plane.

    We do also have some intensive online verification checks, and these are gated behind a comptime flag.

    Finally, we compile Zig with ReleaseSafe and further have all Zig’s own assertions enabled. For example, checked arithmetic for bounds overflow, which is not something you see enabled by default in safe builds for most languages, but which is critically important for safety.

    The reason why all this is so important, is because if your program does something wrong in production, with people’s money, you want to know about it immediately and shutdown safely.

    In other words, production is where you most need the safety, not in development (although you obviously want them there too to find bugs faster). But again, it’s the bugs that make it to production that we’re trying to catch with assertions.

    • Thanks for your reply!

      > it’s the bugs that make it to production that we’re trying to catch with assertions.

      Nicely put, I think I’ll steal this!

      1 reply →

> There are known scenarios in the literature that will cause Postgres to lose data, which TigerBeetle can detect and recover from.

What are you referencing here?

  • The scenarios described in our QCon London talk linked above.

    This surveys the excellent storage fault research from UW-Madison, and in particular:

      “Can Applications Recover from fsync Failures?”
    
      “Protocol-Aware Recovery for Consensus-Based Storage”
    

    Finally, I'd recommend watching “Consensus and the Art of Durability”, our talk from SD24 in NYC last year:

    https://www.youtube.com/watch?v=tRgvaqpQPwE

    •     [disks are] somewhere between non-byzentine fault tolerance and
          Byzantine fault tolerance ... you expect the disk to be almost 
          an active adversary ...
          ...
          so you start to see just a single disk as a distributed system
      

      My goodness, not at all! If you can't trust the interface to a local disk then you're lost just at a fundamental level. And even ignoring that, a disk is an implementation detail of a node in a distributed system, whatever properties that disk may have to that local node are irrelevant in the context of the broader system, and are the responsibility of the local node to manage before communicating anything with other nodes in that broader system.

      Combined with https://www.youtube.com/watch?v=tRgvaqpQPwE it seems like the author/presenter is conflating local/disk-related properties/details with distributed/system-based requirements/guarantees. If consensus requires a node to have durably persisted some bit of state before it sends a particular message to other nodes in the distributed system, then it doesn't matter how that persistence is implemented, it only matters how that persistence is observable, disks and FS caches and etc. aren't requirements, they're just one of many possible implementation choices.

      7 replies →