Strange Loop Conference
Distributed systems cannot guarantee transactional reliability unless developers eliminate real-world hardware entropy and validate production logic inside an accelerated, deterministic simulator.
AI-assisted summary of "Testing Distributed Systems w/ Deterministic Simulation" by Will Wilson — original source. Check the original for full context.
Source published:
Key ideas
- FoundationDB spent its first two years building a completely deterministic simulation before writing the actual database engine, ensuring all concurrency and network interactions were testable inside a single operating system process. (4:03)
- The team created Flow, an actor-model C++ syntactic extension and precompiler, to avoid callback hell and provide deterministic, single-threaded pseudo-concurrency without hardware thread-race conditions. (7:16)
- FoundationDB runs 5 to 10 million simulation runs nightly—the equivalent of trillions of real-world CPU hours—by accelerating failure frequencies and skipping idle simulated time. (28:26)
- Physical validation on the 'Sinkhole' power-cycling cluster uncovered a power-safety bug in Apache ZooKeeper and a missing fsync bug in Linux's apt package manager, while finding zero power-safety bugs in FoundationDB itself. (33:02)
Detailed notes
## Background Presented by Will Wilson of FoundationDB, this talk was published by the Strange Loop Conference on YouTube on September 20, 2014. According to the description, debugging concurrent distributed systems in noisy network environments is nearly impossible through purely analytical or empirical means, making deterministic simulation—where hardware components like disks and networks are swapped for fully transparent software contracts—a powerful method for indefinitely reproducing and verifying complex failure modes.
## TL;DR Will Wilson outlines FoundationDB's architectural choice to build a fully deterministic simulation framework before implementing the physical database itself, ensuring transactional ACID guarantees could be reliably tested. To overcome thread-scheduling randomness while preserving C++ performance, the team built Flow, an actor-model language extension compiling asynchronous routines into single-threaded state machines. By replacing network and disk interfaces with deterministic models, the engine compresses years of operational time into minutes, injecting synthetic disasters, correlated cascading failures via the Hurst exponent, and randomized contract permutations using a `buggify` macro. While deterministic simulation drastically accelerates bug discovery across millions of nightly runs, it forces engineers to rely on deterministic `printf` logging and complementary physical chaos testing via a dedicated hardware cluster nicknamed 'Sinkhole.'
## Atmosphere & style Wilson's presentation is dryly humorous, deeply practical, and grounded in the shared trauma of distributed systems debugging, opening with the quip that debugging distributed systems is only narrowly preferable to sticking a fork in one's eye. His narrative style balances academic depth—such as applying 1950s hydrology statistics to disk crash correlations—with candid confessions about the absurdity of spending years writing a simulator for a nonexistent database. The delivery feels refreshingly honest and battle-hardened, giving the audience an authentic look at the sheer engineering discipline, developer friction, and burned-out hardware required to guarantee database correctness.
## Bulletin - [04:03](https://www.youtube.com/watch?v=4fFDFbi3toc&t=243s) FoundationDB spent its first two years building a completely deterministic simulation before writing the actual database engine, ensuring all concurrency and network interactions were testable inside a single operating system process. - [07:16](https://www.youtube.com/watch?v=4fFDFbi3toc&t=436s) The team created Flow, an actor-model C++ syntactic extension and precompiler, to avoid callback hell and provide deterministic, single-threaded pseudo-concurrency without hardware thread-race conditions. - [28:26](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1706s) FoundationDB runs 5 to 10 million simulation runs nightly—the equivalent of trillions of real-world CPU hours—by accelerating failure frequencies and skipping idle simulated time. - [33:02](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1982s) Physical validation on the 'Sinkhole' power-cycling cluster uncovered a power-safety bug in Apache ZooKeeper and a missing fsync bug in Linux's apt package manager, while finding zero power-safety bugs in FoundationDB itself.
## Key takeaways - [00:01](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1s) Debugging distributed systems is notoriously difficult because uncontrolled environmental entropy—such as delayed packets or reordered transmissions—destroys test repeatability. - [04:03](https://www.youtube.com/watch?v=4fFDFbi3toc&t=243s) ACID transactional guarantees cannot fail safely in production, prompting the decision to simulate the distributed environment before implementing database operations against real hardware. - [07:16](https://www.youtube.com/watch?v=4fFDFbi3toc&t=436s) To achieve single-threaded determinism without thread race non-determinism, FoundationDB developed Flow, an actor-model concurrency precompiler for C++. - [12:00](https://www.youtube.com/watch?v=4fFDFbi3toc&t=720s) Hardware communication interfaces for disks and networks were replaced with deterministic software implementations that programmatically inject random connection drops and latency. - [13:50](https://www.youtube.com/watch?v=4fFDFbi3toc&t=830s) Absolute determinism requires deriving all system randomness from a controllable, seeded PRNG; unseeded time lookups or free disk space queries completely break repeatability. - [13:50](https://www.youtube.com/watch?v=4fFDFbi3toc&t=830s) To verify reproducibility, approximately 1% of all simulation tests are run twice with identical seeds to ensure their final PRNG states match perfectly. - [15:59](https://www.youtube.com/watch?v=4fFDFbi3toc&t=959s) Workload tests, such as mutating a closed ring of keys and values, check invariants to catch atomicity and isolation violations without requiring the system to crash. - [19:11](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1151s) Simulations subject nodes to gradual degradation, whole-datacenter dropouts, and human error such as atomic IP address swaps or data file exchanges. - [20:42](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1242s) Because real customers possess vastly more CPU hours than a developer lab, simulation testing must achieve a far higher failure density per CPU hour to discover bugs first. - [22:04](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1324s) Time acceleration compresses testing cycles by forcing disks to fail every few minutes and advancing virtual clocks instantaneously across idle periods. - [23:04](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1384s) The buggify macro forces the exploration of subtle edge cases by probabilistically triggering alternate, contract-compliant code paths such as reversed packet order or suppressed timeouts. - [24:51](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1491s) Real-world infrastructure disasters follow high Hurst exponent distributions with fat tails, requiring simulations to model correlated, cascading hardware failures rather than naive Gaussian independence. - [29:25](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1765s) Because interactive debuggers struggle with single-process simulated clusters and callback-heavy code, developers rely on surgical printf logging that behaves identically across repeated deterministic runs. - [30:37](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1837s) The 'Sinkhole' chaos cluster physically stress-tests server motherboards and SSDs using programmable power supplies to catch flaws where simulator models deviate from physical reality. - [33:02](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1982s) ZooKeeper was completely replaced with a native Flow implementation of Multi-Paxos because external third-party software could not participate in deterministic simulation. - [38:16](https://www.youtube.com/watch?v=4fFDFbi3toc&t=2296s) While production chaos testing stresses physical hardware and firmware idiosyncrasies, deterministic simulation provides adversarial control down to the microsecond with zero irreproducible Heisenbugs.
## Detailed walkthrough ### [00:01](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1s) The Fundamental Problem of Debugging Distributed Systems
Will Wilson introduces himself as an engineer at FoundationDB, a company building a scalable, fault-tolerant database with ACID transactions. Rather than discussing the database itself, he focuses on simulation testing and why it can dramatically simplify an engineer's life. He opens with a universally relatable observation about engineering pain: “debugging distributed systems is preferable to sticking a fork in your eye, but not that preferable it's actually like it's actually surprisingly close.” While people often blame the sheer complexity of distributed architectures for this misery, Wilson argues that complexity is merely hard; it is not the true root cause of why distributed systems are uniquely impossible to debug.
```mermaid flowchart LR accTitle: Asymptomatic Bug Manifestation accDescr: Server on the right sends packets that get delayed or reordered by network anomalies, triggering catastrophic failure on the server on the left.
subgraph NetCondition["Network Chaos (Entropy)"] direction TB Delay["Time Warp / Lost Packet"] Reorder["Out-of-Order Delivery"] end
SR["Server (Right)"] -->|"Packet(s)"| NetCondition NetCondition -->|"Retry or Delayed Delivery"| SL["Server (Left)"] SL -->|"Unhandled Edge Case"| Boom["Explosion / Data Loss"] ```
To illustrate the actual culprit, Wilson presents a simple scenario involving two nodes: a server on the left and a server on the right. The server on the right sends a non-idempotent packet—meaning receiving it twice produces a fundamentally different outcome than receiving it once. The server on the left contains an asymptomatic bug: it fails to verify how many times the packet has arrived. Because networks usually behave, the bug remains dormant. However, the moment a real-world network hiccup occurs—such as a misconfigured switch or a router routing traffic on a bizarre detour through China—the original packet vanishes into a temporary time warp. The system initiates a retry as intended, but then the delayed packet unexpectedly emerges. The server processes the duplicate, causing the system to crash, corrupt data, or explode.
When the developer attempts to reproduce and debug the failure, they restart the environment, but the system runs flawlessly. Because the triggering condition was an extraordinarily transient network anomaly requiring microsecond-precise timings, the developer cannot reproduce the failure locally. Worse, even if a putative fix is written, there is no way to verify that it actually solved the underlying issue. Wilson reinforces this with a second example: the right-hand server sends packet `A` followed by packet `B`. Although the protocol specification does not guarantee order of arrival, developers naturally write linear code (`send(A)` then `send(B)`) rather than rolling dice to randomize transmission order. As a result, the receiving server implicitly assumes `A` will always arrive before `B`. When packet `A` gets delayed and `B` arrives first, the system breaks in an unrepeatable manner.
Wilson summarizes the underlying cause: “the surface level problem is that we have no repeatability are debugging the real problem is that the messy dirty Universe has intruded on our beautiful pristine land of pure functions right like the execution history of this system is not a pure function of its inputs there's a source of entropy a source of Randomness which you do not control which is the network.” This intrusion of uncontrolled entropy applies equally to threads, disks, and operating systems.
[04:03](https://www.youtube.com/watch?v=4fFDFbi3toc&t=243s) The FoundationDB Philosophy: Simulation Before Implementation
For FoundationDB, building a distributed transactional database made uncontrolled non-determinism an existential issue. Wilson highlights that ACID transactional guarantees are not emergent behaviors that occur by happenstance; there are exponentially more ways to build them incorrectly than correctly. More terrifyingly, there are countless ways to implement them so that they appear correct during normal execution but fail completely the instant an unpredicted environmental failure occurs. Furthermore, unlike a stateless web server or caching layer where operators can casually nuke a malfunctioning virtual machine and spin up a replacement, database users are never philosophical about losing their data.
Consequently, the founding team made a radical architectural decision: if they could not solve the problem of deterministic testability, they should not build a database at all. Instead of writing a database engine from day one, they spent the first couple of years building a 100% deterministic simulation of a database. Wilson acknowledges how absurd this sounds: “for actually a couple years there was no database there was just a simulation that probably sounds totally insane I promise you it's more insane than you think it is.” Only after the simulated architecture was exhaustively debugged did they connect it to real physical disks and physical network interfaces.
Wilson outlines the three massive software engineering challenges required to simulate not just a single process, but an entire distributed cluster of interacting processes, networks, disks, and operating systems within a single physical operating system process:
1. **Single-threaded pseudo-concurrency**: Simulating dozens of concurrent virtual nodes within one single physical OS process without introducing non-deterministic execution schedules from hardware threads. 2. **Simulated environmental interfaces**: Providing deterministic software replacements for all communication interfaces (networks, disks, timers, and clocks) so that every drop of randomness in the system is completely under developer control. 3. **Deterministic process logic**: Ensuring that the internal logic of the simulated processes themselves remains strictly deterministic.
[07:16](https://www.youtube.com/watch?v=4fFDFbi3toc&t=436s) Flow: Actor-Model Concurrency for C++
To achieve single-threaded pseudo-concurrency, FoundationDB ruled out multi-threading due to scheduling non-determinism and dismissed coroutines due to practical implementation issues at the time. This left callback-driven architectures. However, Wilson explains that FoundationDB required C++ for raw execution speed, creating a major dilemma: “callbacks plus C++ is like a nightmarish hellscape um was not something that we wanted to deal with what we needed were higher level Primitives um some abstractions that we could actually reason about that we could actually write and actually read.”
```mermaid flowchart TD accTitle: Flow Compilation Pipeline accDescr: Flow source with actor keywords compiles to standard C++ callbacks and state machines.
A["Flow Source (.actor.cpp)<br/>actor keyword, async/wait, apparent plain return"] -->|"Flow Precompiler"| B["Standard C++ Code (.actor.g.h /.g.cpp)<br/>Generates class, constructor, returns Future<T>"] B -->|"Standard C++ Compiler"| C["Executable with Deterministic<br/>Event-Loop Runtime"] ```
To bridge this gap, the team created **Flow**, an abstraction that sits halfway between a library and a programming language. Flow is a custom syntactic extension and pre-compiler targeting C++ that allows engineers to write asynchronous code using actor-model concurrency while compiling it down into single-threaded, callback-driven state machines.
Wilson walks through the simplest conceivable Flow actor: an asynchronous addition routine (`asyncAdd`). The code accepts a `Future<float>` and a plain `float`, waits for the future value to resolve, and returns their sum. He highlights two syntax peculiarities that differentiate Flow from vanilla C++: - The `actor` keyword flags the block to the Flow compiler so it knows to transform the function into an actor class and state machine. Passing this keyword into a standard C++ compiler would produce a syntax error. - The declared return type is `Future<float>`, yet the body contains what looks like a synchronous return of a standard `float`. The function body never runs directly as written; it acts declaratively. Flow synthesizes a wrapper function that immediately returns an unfulfilled `Future`, which only completes when the underlying asynchronous operations finish.
Behind the scenes, the Flow precompiler strips the original body, adds `const` modifiers to parameters, instantiates a newly generated actor class, passes the parameters to its constructor, and ties the lifecycle into the single-threaded event loop.
- Flow acts as a precompiler targeting standard C++. - The actor's return type in Flow represents the eventual asynchronous completion (`Future<T>`), allowing concise, linear syntax for asynchronous operations. - All actor scheduling runs sequentially within a single thread, eliminating thread-race non-determinism while preserving readable, asynchronous control flow.
[09:42](https://www.youtube.com/watch?v=4fFDFbi3toc&t=582s) Under the Hood of Flow and the Ring Benchmark
When compiling Flow code, the generated actor class returns immediately, and the actual implementation of the actor is broken up across various methods within the class. Every time the code encounters an `await` statement—where it must wait for an asynchronous process to terminate—the current function in the class ends. It sets a callback on that future pointing to the next part of the actor, and then yields immediately to a centralized scheduler so the rest of the program can continue running. At the very end of this process, the actor fulfills the future it was holding with its promise, destroys itself, and cleans up. This entire asynchronous flow runs efficiently within a single thread.
To demonstrate the speed of this approach, Will Wilson shares performance numbers from the "Ring Benchmark," originally designed by Erlang creator Joe Armstrong. In this benchmark, $N$ actors are arranged in a ring, and they pass a message to one another all the way around the ring $M$ times. Wilson warns the audience to take these benchmark numbers with a "gigantic grain of salt" for two reasons: first, the non-Flow comparison numbers were pulled from the internet and run by other teams, and second, the benchmark itself is highly simplistic.
As actors become more complex and incorporate more asynchronous logic in their bodies, Flow's relative performance advantage decreases. This limitation is what has driven the team to begin writing Flow 2 to achieve even greater speeds.
> "I hope Joe Armstrong's not here, is he? It's kind of a dumb benchmark... like I mean it doesn't actually model like what real distributed actor systems do very much."
- **The Ring Benchmark:** A test where $N$ actors pass a message around a ring $M$ times to measure message-passing latency and overhead. - **Flow Actor Compilation:** Translates asynchronous `await` statements into a series of callbacks and state transitions, yielding execution back to a single-threaded scheduler. - **Flow 2:** An ongoing development effort to maintain high performance even as actor complexity and internal asynchrony scale up.
---
[12:00](https://www.youtube.com/watch?v=4fFDFbi3toc&t=720s) Simulating Hardware and Injecting Network Chaos
Because FoundationDB was already designed as a multi-platform program, it featured clean, abstracted interfaces for interacting with disks and networks. This architectural separation made it incredibly easy to drop in simulated implementations right alongside the real Unix and Windows networking code. In the simulation environment, attempting to establish a network connection does not open a real socket; instead, it initializes a C++ class containing internal state and buffers. To simulate network latency, the simulator waits for a short period on the sending side, waits again on the receiving side, copies the bytes, and finally notifies the receiver that data is available.
```mermaid flowchart TD accTitle: Simulated Network Connection with Latency and Fault Injection accDescr: A flowchart showing how a simulated network connection processes data, applies latency, and randomly injects faults.
Start["Send Data"] --> LatencySend["Simulate Sender Latency"] LatencySend --> RollLoss{"Roll Random Close?"}
RollLoss -->|Unlucky / True| InjectFault["Inject Fault: Close Connection / Return Error"] RollLoss -->|Lucky / False| LatencyRecv["Simulate Receiver Latency"]
LatencyRecv --> CopyBytes["Copy Bytes to Buffer"] CopyBytes --> Notify["Notify Receiver: Bytes Available"] ```
The true power of this simulated network lies in its ability to inject faults. Within the simulated network's `read` method, the code makes a call to a function called `roll_random_close`. Every time a process attempts to read from the simulated network, the system rolls a pseudo-random number. If the run is "unlucky," subsequent random rolls determine a specific failure mode: the peer might abruptly close the connection, the local side might close it, or the connection might fail entirely with an error. This chaotic environment is designed to aggressively flush out any latent assumptions about network reliability.
> "This is like how we try to smoke out any code in our system that might have been assuming the network was reliable, which is a very foolish thing to assume..."
---
[13:50](https://www.youtube.com/watch?v=4fFDFbi3toc&t=830s) The Elusive Quest for Absolute Determinism
Achieving complete determinism sounds straightforward in theory, but non-determinism creeps into software in incredibly subtle ways. The most obvious source is random number generation, such as the random numbers used to calculate exponential backoff. To keep this deterministic, all randomness must flow from a pseudo-random number generator (PRNG) that the developers control and seed. The seed of this PRNG then becomes a direct input to the program, ensuring that the exact same seed always produces the exact same execution history.
However, randomness is not the only culprit. Simply checking the system time inside an `if` statement or querying the operating system for free disk space instantly introduces non-determinism, causing two different runs of the same program to diverge. Despite rigorous training, programmers still make mistakes and introduce non-deterministic behavior. To combat this, FoundationDB runs approximately 1% of its simulation tests twice using the exact same inputs. At the end of the dual runs, the engine compares the state of the PRNGs; if the next numbers they would have generated do not match perfectly, a determinism failure is flagged and investigated.
> "Our programmers are trained to recognize this stuff and to try and avoid it, and we still screw it up all the time — really all the time..."
- **Sources of Non-Determinism:** System time checks, free disk space queries, unseeded random number generators, and local OS resource checks. - **PRNG Control:** All simulation randomness is derived from a single, seeded pseudo-random number generator, making the seed a critical input for test replication. - **Double-Run Verification:** A testing safety net where 1% of simulations are run twice with identical inputs to verify that the final internal states match exactly.
---
[15:59](https://www.youtube.com/watch?v=4fFDFbi3toc&t=959s) Test Files, Cycle Tests, and the "Scary" Swizzle Flag
To find bugs, the simulation framework utilizes test files that declare two opposing forces: a workload that the system is trying to achieve, and a set of simulated disasters designed to prevent it. A prime example of a workload is the "cycle test." In this test, a ring of keys and values is inserted into the database, where each value points to the next key in the loop. The system then executes concurrent transactions at a rate of 1,000 per second. Each transaction mutates the keys and values but must preserve the integrity of the overall ring. This setup provides a simple, elegant invariant to check at the end of a run: if ACID properties are violated via an atomicity or isolation failure, the ring will be broken, shortened, or lengthened.
```mermaid flowchart TD accTitle: Cycle Test Invariant Ring accDescr: A diagram showing a ring of keys and values in a database being concurrently mutated by transactions while maintaining the ring structure.
subgraph Database ["Database State (The Ring)"] K1["Key 1"] -->|points to| K2["Key 2"] K2 -->|points to| K3["Key 3"] K3 -->|points to| K1 end
Tx1["Transaction 1 (Mutates Keys)"] -.->|Must Preserve Ring| Database Tx2["Transaction 2 (Mutates Keys)"] -.->|Must Preserve Ring| Database ```
While the cycle test runs, the simulation file unleashes a battery of faults. It performs "random clogging," which pseudo-randomly stops simulated network connections from sending or receiving packets. This test also utilizes a "swizzle" flag—a variation of clogging where a subset of network connections is stopped on a rolling basis and then brought back online in reverse order.
Additionally, the test features "attrition," which simulates power failures by killing up to 10 machines over the course of a 30-second test while ensuring at least three remain online. Most deviously, the test triggers a database configuration change mid-run, forcing the cluster to execute Multi-Paxos coordination changes while machines are rebooting and network connections are being clogged and swizzled.
> "For reasons that we totally don't understand, this is better at finding bugs than normal clogging — it's kind of scary."
- **Cycle Test Invariant:** A database workload where concurrent transactions mutate a closed loop of key-value pairs; any violation of ACID properties breaks the loop, making transactional bugs easy to detect without a system crash. - **Swizzling:** An advanced network clogging pattern that pauses connections on a rolling basis and restores them in reverse order, which has proven highly effective at exposing distributed coordination bugs. - **Attrition and Reconfiguration:** Simulates physical machine power-offs and reboots alongside database configuration changes, forcing the system to run Multi-Paxos consensus under extreme network and hardware stress.
[19:11](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1151s) Torturing the Database with Extreme Failure Modes
FoundationDB subjects its database to highly aggressive, simulated failure scenarios while simultaneously pushing a heavy workload of 1,000 transactions per second. The goal is to expose deep architectural flaws, such as bugs in their Paxos implementation, before the software ever reaches production. Rather than simply shutting down machines, the simulation engine can transition nodes into a state of gradual degradation. For example, the engine can inject a rule where all future system calls on a specific machine have a 1% chance of returning an error. This allows the team to verify if the cluster is intelligent enough to detect the failing node, quarantine it, and destroy it before it can infect the rest of the system.
The simulation framework also tests graceful shutdowns and leverages its awareness of network and physical topology. Because FoundationDB understands locality—such as rack configurations and WAN links—the simulation can mimic the catastrophic loss of an entire datacenter to verify that data remains durable. Additionally, the team simulates what they jokingly refer to as "dumb sysadmins," a phenomenon they dryly note "totally don't exist in the real world." To test resilience against human error, the simulation can atomically swap the IP addresses of two active machines, or instantly copy and swap the data files of two computers to see if any data loss occurs.
Will Wilson describes the joy of designing these brutal scenarios: “...one of the most fun parts of my job is thinking up new and creative ways of torturing our database and then implementing that in simulation...”
- **Gradual Degradation Simulation:** System calls are configured with a 1% failure rate to test the cluster's ability to quarantine failing nodes. - **Topology-Aware Failures:** The simulation can wipe out entire simulated datacenters to test WAN durability. - **Human Error Simulation:** The framework atomically swaps IP addresses or hard drive data files between active machines.
[20:42](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1242s) The CPU Hour Race Against the Real World
The fundamental challenge of relying on simulation is the sheer scale of the state space. While a cluster of test machines diligently runs simulations to explore possible inputs and failure modes, the real-world deployment scale is vastly larger. Customers run distributed systems on highly imperfect infrastructure, and they do so at a scale that dwarfs any internal test lab. The engineering team must find a way to explore this massive state space and uncover bugs faster than their entire customer base combined.
To stay ahead, the testing framework must achieve a bug-discovery rate per CPU hour that is many orders of magnitude higher than that of the real world. If the customer base has access to far more CPU hours, the simulation must compensate by being incredibly dense with failures and edge cases, turning what would be rare, multi-year real-world events into routine, minute-by-minute occurrences.
Will Wilson highlights the harsh reality of shipping software to customers: “...your customers are running your distributed systems on buggy crashy failey grossly underprovisioned totally improperly configured Hardware and they're doing insane things to it by the way and PS if it breaks it's your fault.”
```mermaid flowchart TD accTitle: Simulation vs. Real World Bug Discovery accDescr: Comparison of how simulation compresses time and increases failure density to find bugs faster than customers in the real world. subgraph RealWorld ["The Real World"] A["Millions of Customer CPUs"] -->|Slow Failure Rate: Disks fail every 2-3 years| B["Bugs Found Over Years"] end subgraph SimWorld ["Deterministic Simulation"] C["Internal Test Cluster"] -->|High Failure Rate: Disks fail every 2-3 minutes| D["Time Acceleration & Bugification"] D -->|Fast Failure Rate| E["Bugs Found in Hours"] end ```
- **Scale Discrepancy:** Customers possess orders of magnitude more computing power than the developer's test lab. - **The Goal:** Achieve a significantly higher bug-detection rate per CPU hour than the real world through artificial acceleration.
[22:04](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1324s) Accelerating Failures and Speeding Up Time
To compress years of real-world operations into hours of testing, FoundationDB employs two primary strategies: accelerating the frequency of failures and speeding up simulated time. In a production environment, a hard drive might fail once every two or three years under heavy load. In the simulation, the engine forces disks to fail every two or three minutes. This ensures that the code responsible for handling disk failures is executed and tested far more frequently per CPU hour than it ever would be in a standard deployment.
The second strategy is "cheating" by accelerating time itself. Distributed systems often spend significant periods idling, waiting for timeouts, or recovering from failures. When the simulated cluster enters a quiescent state where it is simply waiting for a hardcoded number of seconds to pass, the simulation engine skips the idle physical time. It instantly advances the simulated clock, allowing many simulated seconds to pass in a fraction of a real-world second, limited only by the processing power of a single CPU core.
Will Wilson explains the philosophy behind this acceleration: “...in the real world how often does a disc fail like every two or three years if it's under a heavy load or something we make them fail like every two or three minutes...”
- **Compressed Failure Cycles:** Disk failures are accelerated from a multi-year real-world cadence to a 2-to-3-minute simulation cadence. - **Time Skipping:** Idle and quiescent states are bypassed by instantly advancing the simulated clock.
[23:04](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1384s) Bugification: Exposing Asymptomatic Bugs
Even with accelerated failures, some bugs remain asymptomatic because they require highly specific, rare conditions to manifest. To combat this, FoundationDB uses an in-house technique called "bugification." This process involves intentionally and randomly altering the behavior of the code during simulation to ensure that interfaces are robust and do not rely on implicit, narrow assumptions.
Bugification is implemented via a `buggify` macro. In simulation, this macro evaluates to true with a small probability, using a hash of the line number to maintain determinism. For example, a developer might write code that occasionally reverses the order of sent packets or deliberately suppresses a timeout. By forcing the system to handle these unusual but contract-compliant behaviors, the team ensures that the system can survive extreme edge cases.
Will Wilson describes the counterintuitive nature of this technique: “...the idea here is you do just that thing which I said programmers never do which is sometimes randomly change what your code does...”
```mermaid flowchart TD accTitle: Bugification Macro Logic accDescr: Flowchart showing how the buggify macro randomly alters code execution paths during simulation to test edge cases. A["Code Execution Path"] --> B{"Is Simulation & buggify active?"} B -->|Yes (Small Probability)| C["Altered Behavior (e.g., Swap packet order, suppress timeout)"] B -->|No| D["Standard Behavior (Contract-compliant path)"] C --> E["Verify System Resilience"] D --> E ```
- **The `buggify` Macro:** A compiler macro that randomly activates alternative, valid execution paths during simulation. - **Interface Testing:** Forces systems to handle out-of-order packets and missing timeouts to prevent implicit dependencies.
[24:51](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1491s) The Hurst Exponent and Cascading Failures
To explain why naive testing fails to capture real-world disaster scenarios, Will Wilson introduces the concept of the Hurst exponent. First described by British civil servant Harold Edwin Hurst in his 1951 paper *"Long-Term Storage Capacity of Reservoirs"* (published in the *Transactions of the American Society of Civil Engineers*), the exponent was developed while studying the Nile River's dams. Previous engineers assumed that annual rainfall events were independent, uncorrelated trials, leading them to design reservoirs for "100-year" floods and droughts. However, Hurst observed that these extreme events were actually occurring every 15 years.
The discrepancy exists because real-world events are highly correlated; the probability of rain on Wednesday is higher if it rained on Tuesday. This lack of independence yields stable Paretian distributions with fatter tails than a standard Gaussian distribution. In a Gaussian distribution (where the Hurst exponent is 0.5 and the alpha parameter is 2), extreme "high-sigma" events are virtually impossible. When the Hurst exponent exceeds 0.5, the fatter tails mean these extreme events occur far more frequently.
In distributed systems, hardware failures are similarly correlated. If a hard drive fails, it is often due to a bad manufacturing batch, poor maintenance, or environmental factors like high humidity, meaning other drives in the same rack are highly likely to fail soon after. While physical testing of cascading, correlated failures is incredibly difficult, FoundationDB's simulation framework easily manipulates the Hurst exponent to generate highly correlated, catastrophic failure cascades.
Will Wilson emphasizes the non-random nature of infrastructure failures: “...the problem is that multiple cascading failures which a non-naive approach to statistics tells us are much more likely than a simple naive Gaussian would predict are among the very very hardest things to test in real world physical testing...”
- **Harold Edwin Hurst (1951):** Discovered that natural events are correlated, invalidating the independent-trial assumptions used in reservoir engineering. - **Fatter Tails:** A Hurst exponent greater than 0.5 results in stable Paretian distributions where extreme events occur much more frequently than a Gaussian distribution predicts. - **Correlated Hardware Failures:** Simulation allows the team to artificially increase the correlation of failures to mimic real-world cascading disasters.
[28:26](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1706s) Trillions of Hours and the Curse of Callback Debugging
By combining time acceleration, aggressive failure injection, and bugification, FoundationDB achieves an extraordinary testing scale. The team runs between 5 and 10 million simulation runs every single night on their testing cluster. Each run simulates an entire cluster operating for anywhere from five minutes to two hours. Over years of continuous testing, they estimate they have executed the equivalent of trillions of real-world CPU hours.
However, this powerful testing methodology introduces a major drawback: it completely breaks traditional debugging. Stepping through highly concurrent, asynchronous code that is heavily reliant on callbacks is already notoriously difficult for developers. Attempting to use a traditional debugger to step through a deterministic simulation of callback-infused code is significantly worse, creating what Wilson describes as "callback infected monstrosities" that defy standard debugging techniques.
Will Wilson laments the developer experience in this environment: “...you all already know that stepping through code that is laced and infused with callbacks is horrible if you were to take a traditional debugging approach to a deterministic simulation of your call back infused code that'll be much worse.”
- **Testing Scale:** 5 to 10 million simulation runs executed nightly, representing trillions of equivalent real-world CPU hours over the project's lifespan. - **Simulation Duration:** Each run simulates between 5 minutes and 2 hours of cluster lifetime. - **The Debugging Tradeoff:** Traditional step-through debugging is rendered highly impractical due to the complex, callback-heavy nature of the simulated asynchronous code.
[29:25](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1765s) Debugging in Simulation: The Surprising Power of Deterministic `printf`
Debugging distributed systems inside an attached debugger like GDB is notoriously difficult. When running a simulated cluster, multiple simulated processes coexist within the exact same operating system process you are attached to, alongside all the simulation harnesses busily convincing those nodes that they are secretly communicating with one another. While a few exceptional engineers on the FoundationDB team can somehow step two or three levels down the call stack in such an environment, the majority cannot. Consequently, developers are forced to rely heavily on `printf` logging.
```mermaid flowchart TD accTitle: Deterministic Printf Debugging Cycle accDescr: Flowchart illustrating how deterministic simulation enables targeted printf debugging without log spam.
subgraph Iteration["Debugging Workflow"] A["Reproduce Bug with Fixed Seed"] --> B["Add Precise, Conditional printf Statements"] B --> C["Re-run Simulation with Identical Input"] C --> D["Observe Exact Event Order at Exact Timestamps"] D --> E["Narrow Scope and Eliminate Bug"] end ```
While relying on print statements sounds primitive, it functions remarkably well under deterministic simulation. Because rerunning a run with the exact same inputs reproduces identical events in the exact same order at the exact same virtual times, developers can write surgical, non-spammy log statements tied to specific known preconditions. Wilson explains:
> “What you're left with is basically print F uh which sounds pretty horrible and barbaric uh and I'll be honest it is um but it's actually not that bad when you have deterministic simulation and you know that if you rerun the simulation with the exact same inputs the exact same set of stuff will happen in the exact same order at the exact same times.”
This repeatability enables engineers to incrementally drive their logging deeper into the code path, corner the elusive bug, fix it, and verify that it no longer occurs under that exact seed.
---
[30:37](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1837s) The Limits of Simulation and the "Sinkhole" Chaos Cluster
Deterministic simulation has an inherent blind spot: the simulation can simply be wrong. Wilson highlights two distinct failure modes for a simulation framework: 1. **Insufficient brutality:** The simulation may not model bizarre real-world network phenomena that occur in the wild. 2. **Flawed contract assumptions:** The developers might misinterpret operating system semantics (such as subtle differences in what `fsync` guarantees across platforms) or hit an unexpected OS-level bug.
To catch discrepancies between simulated behavior and actual physical hardware, the team constructed an on-premise hardware validation cluster nicknamed "Sinkhole."
```mermaid flowchart LR accTitle: Architecture of the Sinkhole Testing Cluster accDescr: Hardware components of the Sinkhole chaos-testing cluster.
subgraph Cluster["Sinkhole Hardware Cluster"] direction TB MB["Server Motherboards<br>(Real Hardware w/ ECC RAM)"] SW["Network Switches<br>(Connected via Cat 5)"] PDU["Programmable Network<br>Power Supplies"] SSD["Host SSDs"] end
PDU -.->|"Randomized Hard Power Cycling"| MB PDU -.->|"Randomized Hard Power Cycling"| SW MB --- SSD ```
Sinkhole consists of small real-server motherboards equipped with ECC memory, wired through network switches with Cat 5 cables. Both the switches and the motherboards are plugged into programmable, networked power distribution units. The team programs these power supplies to relentlessly cycle power on and off all night while the database executes transactions under heavy load.
This brutal physical testing revealed practical operational headaches: - Network power distribution units burn out frequently under constant cycling: “Network power supplies suck they burn out after like two days it's a giant pain in the ass then we have to buy more we've run through hundreds of these things.” - Rapid power cycling rapidly degrades and destroys solid-state drives (SSDs). - Despite burning through hardware, the Sinkhole cluster has never uncovered a power-safety bug in FoundationDB itself, providing strong validation for the simulator's efficacy.
---
[33:02](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1982s) Finding Bugs in Upstream Software: ZooKeeper and `apt`
While Sinkhole did not catch bugs in FoundationDB's own engine, it exposed critical bugs in third-party software that the team could not test inside their deterministic simulator. Any software written outside of their custom C++ actor framework (Flow) cannot participate in simulated scheduling.
- **Replacing Apache ZooKeeper:** Early FoundationDB builds used ZooKeeper for coordination state. A predecessor to the Sinkhole cluster uncovered a power-safety bug in ZooKeeper. Although the upstream team resolved the issue after FoundationDB filed a bug report, the scare was severe enough that FoundationDB completely ripped ZooKeeper out of their codebase. They wrote their own implementation of Paxos natively in Flow, allowing coordination to run directly within the simulation framework (with the added bonus of eliminating Java deployment friction). - **The Missing `fsync` in Package Managers:** In another Sinkhole trial, a node dropped offline and refused to restart after a power cut. The team investigated, puzzled, since the database was proven to be power-safe. They discovered the transactional database files had all been durably synced to disk, but the configuration files were corrupted or missing entirely. The operating system's package manager (`apt`) had omitted calling `fsync` when installing the package files. Upon restarting, the database process detected existing data files without a valid configuration file, assumed an inconsistent state, and safely panicked.
---
[34:34](https://www.youtube.com/watch?v=4fFDFbi3toc&t=2074s) Open Challenges: Developer Ergonomics and Test Suite Evolution
Even with deterministic simulation, exploring the combinatorial state space of distributed failures remains a massive undertaking. Wilson outlines several ongoing challenges and operational friction points:
- **Mutation Testing Teams:** Wilson notes they would love to dedicate engineers strictly to injecting subtle bugs into the codebase to observe whether the simulation caught them, treating failures of detection as actionable bugs against the simulation framework itself. - **Workflow Latency and Cycle Times:** The testing cycle places significant cognitive friction on engineers:
> “If you look at the workflow of one of us it looks like you make some changes to the program you recompile and then you run like 10,000 simulations to see if you did something obviously dumb and then you hope that if you did something subtly dumb you'll get caught that night when we run 10 million that sucks running 10,000 simulations takes like a minute or two or I don't know a while um you lose your train of thought it totally breaks your flow.”
- **Dual-Framework Mutation Resistance:** Because long-lived test suites can inadvertently allow surviving bugs to adapt around specific test heuristics, the team has considered building two distinct simulation frameworks based on different design principles. Developers would iterate against the fast primary framework day-to-day, while a second, independent framework would stress release candidates across tens of millions of runs—analogous to multi-drug protocols used to prevent antibiotic resistance. - **The Misery of Real Hardware:** Despite recognizing the value of physical chaos testing, interacting with physical hardware remains universally hated by developers. A colleague often jokes that all programming is enjoyable except for the code that interacts with networks, disks, or power supplies—yet that work is all he does now.
---
[38:16](https://www.youtube.com/watch?v=4fFDFbi3toc&t=2296s) Q&A: Simulation versus Production Chaos Testing (Chaos Monkey)
An audience member compares FoundationDB’s approach to Netflix's Chaos Army and asks about testing directly in production. Wilson clarifies that all simulation tests run actual, production-ready code. The only code path omitted from simulation is the thin outer boundary that interfaces with physical disks and the network interface cards.
Wilson contrasts the two paradigms: - **Simulation's Advantage (Omnipotent Control):** In a simulator, developers hold godlike, deterministic control over the entire virtual universe. They can inject calculatedly evil, worst-case network partitions and manipulate timing down to the microsecond precisely when Paxos is in its most vulnerable state. Real-world chaos testing rarely achieves this degree of targeted adversarial precision. - **Production Chaos Testing's Advantage (Hardware Fidelity):** Real-world chaos testing directly stresses the idiosyncratic firmware and physical failure modes of commercial disks, cables, and network cards in use across customer sites.
Wilson concludes by acknowledging that both approaches offer unique advantages, though FoundationDB leans into deterministic simulation because of the unmatched precision and reproducibility it affords when tackling subtle distributed concurrency bugs.
## Notable quotes > [20:42](https://www.youtube.com/watch?v=4fFDFbi3toc&t=1242s) your customers are running your distributed systems on buggy crashy failey grossly underprovisioned totally improperly configured Hardware and they're doing insane things to it by the way and PS if it breaks it's your fault. — Will Wilson
## Mentioned Will Wilson, FoundationDB, Flow, C++, Joe Armstrong, Harold Edwin Hurst, Paxos, Multi-Paxos, Apache ZooKeeper, GDB, Netflix, Chaos Monkey, Sinkhole, apt
## Video description Debugging highly concurrent distributed systems in a noisy network environment is an exceptionally challenging endeavor. On the one hand, evaluating all possible orders in which program events can occur is a task ill-suited to human cognition, rendering a pure analytic understanding of the control flow of such a system beyond the reach of any individual programmer. On the other hand, a more “empirical” approach to the task is also fraught with difficulty, as the dependence of severe bugs on precise timings or transient network conditions makes every part of the debugging cycle – from bug replication to verification of a fix – a Sisyphean labor bordering on the impossible.
One approach which has been developed to ameliorate this situation is that of deterministic simulation, wherein the hardware components of the system – including hard disks, network links, and the machines themselves – are replaced in testing with software which fulfills the contracts of those systems, but whose state is completely transparent to the developer. This enables the simulation of a wide diversity of failure modes including network failures, disk failures or space exhaustion, unexpected machine shutdown or reboot, IP address changes, and even entire datacenter failures. Moreover, once a particular pattern of failures has been identified which uncovers a bug, the determinism property of the simulation means that the exact same series of events can be replayed an indefinite number of times, greatly facilitating the debugging process, and providing confidence when a bug has been fixed.
Attendees of this talk will gain an understanding of the benefits, drawbacks, and tradeoffs involved in implementing a deterministic simulation framework, with frequent reference both to theory and to real-world engineering experience gleaned from applying this method to a complex distributed system. Attendees will also learn about language features which aid in the development of such a framework.
Will Wilson FoundationDB
Will Wilson works on the engineering team at FoundationDB (https://foundationdb.com). Will started his career in biotechnology, leading a successful R&D effort in spinal cord injury diagnostics, currently undergoing commercialization by a company he co-founded. Since then, Will has worked in a variety of technical and business roles at data science and data virtualization startups. Will has a degree in math and philosophy from Yale.
[Source](https://www.youtube.com/watch?v=4fFDFbi3toc)