TeardownFault

sealed interface TeardownFault

What a FaultySeam's teardown should do — a different axis from FaultProfile, which describes what happens to frames.

Before #2501 FaultySeam.close was an unconditional passthrough to the delegate, so the one component in the tree whose job is to make a transport misbehave could not make a teardown slow, suspend, or fail. Every obligation that only becomes falsifiable when close misbehaves — Room.leave's idempotency and its no-minted-cancellation obligation (#1826), reached through SeamRoom.leave's seam.close(...) — was therefore unreachable rather than merely unwritten.

Why a separate knob rather than a FaultProfile arm

FaultProfile's model is per-frame evaluation carrying a Direction. A teardown has neither a frame nor a direction, so a teardown arm would be three dead branches in FaultState's outbound / inbound / inbound-delay evaluators plus a teardown-extraction walker alongside inboundDelay for FaultProfile.Composite — conflating two axes to reuse one sealed hierarchy.

Honouring FaultProfile.DelayAll inside close was the other option and is worse: it silently changes teardown at every existing FaultySeam call site, and a delay can only make a close slow, never fail, so the minted-cancellation obligation would still have no failure to classify.

Orthogonal and defaulting to None means no existing call site changes behaviour at all, and slow and failing teardowns compose freely with any frame profile.

Determinism: Slow suspends through kotlinx.coroutines.delay, so kotlinx.coroutines.test.runTest controls it in virtual time — no wall clock is consumed.

Inheritors

Types

Link copied to clipboard
data class Fails(val cause: Throwable) : TeardownFault

Delegate the close, then throw cause.

Link copied to clipboard
data object None : TeardownFault

Teardown is delegated straight through — the pre-#2501 behaviour, and the default.

Link copied to clipboard
data class Slow(val delay: Duration) : TeardownFault

Suspend for delay, then delegate the close.