Somtochi Onyekwere: Separate Protocol Logic from I/O for Deterministic Tests

A Sans I/O design lets a local simulator run thousands of protocol states with virtual time and controlled failures.

SOSomtochi Onyekwere登壇者 · Fly.io
チャンネル
GOTO Conferences
公開日
2026年10月30日
動画
20:04
読了時間
4 分
スライド
4
原文
英語
YouTube で開く
スクロールすると動画は右下に小さくなって再生を続けます。字幕は閲覧言語に合わせて切り替わります。
閲覧言語2 言語で読めます
この記事はまだ日本語に翻訳されていないため、英語の原文を表示しています。
要約

30 秒でわかる

Fly.io engineer Somtochi Onyekwere describes separating Corrosion’s Plumtree broadcast logic from sockets, timers and randomness. A caller-provided runtime enables deterministic simulation, while production integration remains a separate testing responsibility.

  1. 1Events in, actions outThe protocol consumes events and delegates message sends and timers to its caller.
  2. 2A local cluster simulationA time-ordered event heap drives roughly 2,000 node states in one process.
  3. 3Failures as eventsThe test can take nodes down, drop messages and later restore them.
  4. 4Integration still mattersA correct protocol can still fail if the surrounding runtime handles its effects incorrectly.

ポイント

  1. Distributed protocols face nondeterministic message delivery, timing and node failures.
  2. A protocol state machine can accept events and request effects without owning sockets or a clock.
  3. A heap of events and virtual time can simulate many nodes locally.
  4. Injected randomness and failures make scenarios repeatable.
  5. Testing the protocol core does not replace testing the production integration.
全文

チャプターごとに整えた全文を読む

文字起こしのみ
01

Why distributed protocols are hard to test

Networks, timers and node failures create nondeterminism, while large integration environments are expensive.

I work on distributed systems and networking software at Fly.io. This talk is about structuring distributed protocols so that we can test them deterministically and gain confidence that they work before deployment.

Distributed systems are difficult to test because many nodes coordinate over a network. Messages can be delayed, reordered or lost; timers can fire, and nodes can be unavailable. A protocol must preserve its assumptions even when these failures occur.

A large integration test is expensive to run and difficult to reproduce. A cluster with thousands of nodes involves many processes, and a subsequent run may encounter events in a different order. Both scale and nondeterminism make failures hard to investigate.

02

Separate the protocol from effects

The protocol accepts events and requests actions; a caller supplies the runtime.

While changing the broadcast protocol in Corrosion, an open-source SQLite replication system used at Fly.io, we noticed that the protocol described in a paper was relatively simple. We wanted to preserve that small core while making it easy to vary conditions, reproduce failures and reuse the implementation.

Protocol code often mixes its logic with thread creation, message sends and timers. If the implementation sleeps internally, a test may need to sleep too, introducing flakiness. We wanted to move those effects out of the protocol and leave a clear boundary where tests could substitute their own environment.

Events in, actions out
スライド 1 / 4Events in, actions outThe state machine receives events while its environment performs sends and scheduling.

Our model treats the protocol as a state machine. It receives events, such as a message, a peer coming online or a fired timer, and indicates what actions should follow. It does not itself own sockets or a clock; the caller decides how to send messages and schedule timers.

The example is Plumtree, a gossip protocol for broadcasting through a cluster. A node sends full messages to eager peers, which forward them onward. Lazy peers receive message identifiers and can request a missing payload, providing a fallback path.

In Corrosion, an input such as a broadcast request goes to the Plumtree state. The protocol can ask the caller to send messages or deliver a payload to the application. The caller also supplies time, so effects are outside the core logic.

One possible interface would return a list of requested actions. We instead used a trait, or interface, implemented by the caller. It exposes effects such as sending to a node or peers, delivering a payload, notifying the application and scheduling a timer.

The caller-provided runtime
スライド 2 / 4The caller-provided runtimeThe interface exposes network, application and clock effects to the caller.

When the protocol requests a timer, the caller handles scheduling. Once it fires, the caller passes a timer-fired event back to the protocol. The same separation applies to messages: the application provides the runtime and can choose how the effects are performed.

For example, Corrosion buffers messages briefly to batch them efficiently. Another user of the library can choose a different implementation. The integration layer is small: provide the interface and the protocol uses that environment.

03

Simulating a cluster

A local event queue and virtual time run many node states without real sockets or sleeps.

This boundary is especially useful in tests because the test runtime can differ from production. Production can use threads and sockets; a local simulation instead records and queues events. No real network or thread per simulated node is required.

Our test used a binary heap ordered by event time. A loop removes the earliest event, applies it to the appropriate protocol state, and queues the messages, timers or deliveries produced in response. A map holds the state for each simulated node, allowing roughly 2,000 nodes to run on one system.

A runtime that records actions
スライド 3 / 4A runtime that records actionsThe simulation records sends, timers and deliveries for the event loop.

At the start, the test chooses nodes to broadcast at different times and queues those events. Processing one broadcast can generate more sends, timer requests and application deliveries. The loop keeps applying resulting events until the queue is exhausted.

Time is virtual. Instead of waiting for a real timer, the simulator jumps to the time of the next event and tells the protocol that the timer has fired. This makes a long sequence of scheduled behavior fast to execute and repeat.

04

Failures and trade-offs

The simulation can inject failures and measure choices, but integration still requires testing.

Network behavior as events
スライド 4 / 4Network behavior as eventsDelivery, timer, node failure and membership events can be replayed with a seed.

Failures are events too. The test can take a node down, drop messages addressed to it, notify other nodes and bring it back later. That lets us examine how the protocol responds to conditions we explicitly choose.

We also changed Plumtree to fit Corrosion, so we needed to test the consequences. The paper’s membership approach differed from ours, where nodes knew the full membership. We could vary peer selection, including whether to prefer nearby nodes, and measure how that affected broadcast delivery time.

The resulting deterministic test let us exercise and replay protocol behavior before launch. It was not a multiverse debugger, but it gave us a useful local simulation and a systematic way to mock external effects.

There is an important limitation: testing the protocol core does not prove the integration is correct. If a caller blocks while sending, it might fail to pass incoming messages to the protocol promptly. The surrounding integration still needs its own tests.

This approach is often called Sans I/O. It does not eliminate complexity; it makes the boundary and responsibility explicit. Depending on the protocol, separating requested actions from effects can make the library easier to reason about and reuse.

We also inject randomness from the application. Production uses operating-system randomness, while a seeded source makes tests reproducible. Callers must supply the environment and understand how to pass events, so the design adds integration work, but for our case the testing benefits are worth it.

同じチャンネルの記事

スライド 7 枚40:04EN
GOTO Conferences· 2026年11月2日Sophia Drossopoulou: Verifying Trusted Code in an Open WorldSophia Drossopoulou · Imperial College LondonCapabilities, scoped invariants and explicit trust assumptions help reason about effects when trusted code calls unknown objects.Code review & verificationProgramming LanguagesSoftware engineering
スライド 4 枚51:06EN
GOTO Conferences· 2026年11月2日Kent Beck: Build AI Programming Tools for Human NeedsKent BeckThe “genie” can amplify healthy or unhealthy development practices. Beck calls for tools that preserve learning, connection and future options.コーディングエージェントSoftware engineeringDeveloper productivity+1
スライド 3 枚39:12EN
GOTO Conferences· 2026年10月28日Cate Huston: How Rigour Supports AI-Assisted Development VelocityCate Huston · TwillA two-person team’s experience with release gates, automated checks, prioritization and the cost of shipping faster.AI エンジニアリングのリーダーシップSoftware engineeringAI agents
スライド 4 枚22:33EN
GOTO Conferences· 2026年10月26日William Schor: From Unit Tests to Synthetic Canaries at NetflixWilliam Schor · NetflixHow generated workloads and production-like infrastructure help test a distributed key-value service.Software engineering
スライド 5 枚34:11EN
GOTO Conferences· 2026年10月23日Anatoliy Bilenko and Alberto Carretero: Making Database Protocols Reliable with Formal Methods and AIAnatoliy Bilenko, Alberto Carretero · CanonicalCanonical’s database engineers connect executable specifications, model-based tests, failure exploration, and reconstructed execution histories—and explain why none of these techniques proves a real system correct on its own.開発ワークフローとテストCode review & verificationオブザーバビリティと信頼性+2
28:15EN
GOTO Conferences· 2026年10月22日Steve Poole on Open-Source Security and AI’s Maintenance ChallengeOlimpiu Pop, Steve PooleWhy faster vulnerability discovery increases pressure on maintainers, dependency users and upgrade processes.Software engineeringAI security