25 Design for interruption and failure

An operation is easiest to explain when every step returns. The interesting questions begin when a process disappears after changing state but before reporting the result. Did publication fail, or did the acknowledgment fail? Is the worker gone, or has only its caller stopped waiting? Can cleanup proceed, or is some effect still using the files?

These are not exceptional questions appended to a completed design. They determine ownership and observable behavior. If the application promises that an unsuccessful replacement preserves the working version, that promise must survive the interruption points the application claims to handle.

The running example has three broad phases: acquire a candidate, check it, and publish it. That description is enough to start identifying failures, but it is not enough to decide recovery. We also need to know which state is authoritative, which effects can continue independently, and how a later observer distinguishes a completed action from an unattempted one.

Stop treating cancellation as one event

Cancellation can mean that a caller no longer wants a result, that a worker has been asked to stop, that the worker has actually stopped, or that cleanup has finished. Those events can occur at different times. Collapsing them into one boolean invites a resource to be reused while an old effect can still access it.

For example, a blocking checker may outlive the async task waiting for it. Cancelling the waiter does not necessarily terminate the checker. The ownership capability must remain with work that can still use the candidate, and shutdown must account for that work. A timeout provides an observation about waiting; it does not prove cessation.

Passeur's inspected transport distinguishes parent-process exit from stream closure because a descendant can retain the output stream. A controlled native fixture leaves such a descendant alive and expects close to remain unconfirmed. This establishes a specific process boundary in source and test assertions, not a guarantee for every provider's process tree. Transport observation, descendant fixture.

If stop is uncertain, the system needs an explicit unresolved state and a recovery path. Starting a replacement worker immediately may duplicate effects. Waiting forever without exposing the obligation is also a failure of operability. The appropriate response can be to block conflicting mutation, retain evidence, and request the observation or decision that resolves ownership.

Put failure points beside the intended guarantees

A small fault plan for the illustrative acquisition can be written as four cases:

During acquisition: Interrupt before the candidate is complete. The active reference must still open the old version. Partial candidate bytes must remain either owned for a defined recovery path or eligible for safe cleanup.

During checking: Hold a checker, request cancellation, and attempt conflicting reuse. Reuse must remain excluded while the checker can access the candidate.

At publication: Interrupt after the authoritative state might have changed but before the caller receives its result. Recovery must inspect that state and identify the exact candidate; a missing acknowledgment alone cannot justify another publication.

After publication: Interrupt before cleanup or status notification. The published resource must remain usable, and recovery must not remove it while treating the incomplete notification as a failed candidate.

The third case exposes an important qualification to “failure preserves the old version.” A reported error does not prove the publication itself failed. If publication committed, recovery must describe that outcome truthfully. Keeping the previous version available for deliberate rollback can be useful, but silently rolling back on every communication error introduces another state change that also needs authority and evidence.

Retries need the same care. Retrying the same intended acquisition should retain its identity. Starting a new acquisition that happens to use the same URL may select different content if the source is mutable. A retry policy cannot repair an unspecified identity contract.

Match the claim to the fault actually exercised

Pumas's draft acquisition work includes a Linux subprocess test that kills a child after native destination rename and directory synchronization, before metadata publication. The test then checks exact receipt-based recovery without new requests to its controlled source. This is useful evidence about one SIGKILL boundary. It does not reproduce sudden power loss, establish other filesystems' durability, or qualify Windows and macOS. The book's research inspected the test and repository reports rather than rerunning it. Crash-boundary regression.

That limitation is part of the engineering result. Different faults invalidate different assumptions. Throwing an exception preserves the process and much of its memory. Killing a process removes that memory but leaves the operating system alive. Losing power challenges persistence below the process. A network partition creates uncertainty at more than one observer. A claim should name the fault class, environment, and boundary tested.

Failure injection should also be safe for the test environment. Use disposable roots, controlled sources, bounded cleanup, and a way to release held workers when assertions fail. A test that corrupts unrelated data or strands a process has introduced a new lifecycle problem.

Give long-running development its own recovery design

The same reasoning applies to the agent doing the work. Save a useful candidate, current assumptions, failed checks, and the next bounded action where another session can inspect them. A long conversation is not a durable result by itself.

Armin Ronacher's September 2026 account describes an intentionally oversized, largely unattended CPython experiment that ran for about 35 hours and produced extensive code and commits without an outcome he considered useful. He acknowledges the setup's limitations. The episode is a counterexample to equating continued activity with progress, not evidence that all long-running agent work fails. Astra for Coding.

A stop condition should concern the ability to continue usefully: repeated attempts without new evidence, a missing decision, exhausted authorized resources, or an acceptance requirement that the environment cannot exercise. Stopping should preserve the result and explain the blocker. It should not pretend an unfinished task is complete merely because a turn ended.

Interrupt one boundary deliberately

Choose one phase transition in your application and state what must remain true on both sides. Add a controlled interruption, attempt the forbidden competing action, then recover through the public owner. Record the exact environment and fault class.

The observable outcome is a state you can explain after interruption, with resource ownership intact and no invented success. If you cannot tell whether publication occurred, preserve that uncertainty and improve the recovery observation before adding automatic retry.

Search the book

Type to find a passage.