skip to content

A message subscription stops delivering values — how can the code tell a cancelled run from a completed one, and why does the difference matter?

level: seniorimportance: nice to knowfreq 30%

answer

  1. silence is common to both
  2. one is a signal, one is a request
  3. completion handler skips a cancel
  4. abandoned is not success or failure
  5. commit on completion only

basics

~20 s

Completion arrives as a terminal signal from the source; cancellation is a request the subscriber sent upstream, after which it is simply not told anything. So a completion handler never fires on a cancel, while an any-ending hook fires on both and cannot distinguish them by itself.

solid answer

~40 s

The two endings are not two flavours of the same signal. **Completion** is something the source sends downstream: no more values, ended normally. **Cancellation** is something the subscriber sent upstream, and the contract owes it nothing afterwards — it simply stops hearing. That asymmetry decides which callbacks run: a handler attached to completion does not fire when the run was cancelled, and a teardown hook attached to any ending fires for both without saying which. The difference matters wherever the ending carries meaning: marking a conversation fully synchronised, committing a batch, or deciding to retry are all correct on completion and wrong on cancellation, where the user simply closed the screen. When the code needs to know, it must record the cancellation itself rather than infer it from silence.

go deeper

for a junior

Keep the two apart: completing is the source saying there is no more, cancelling is you saying you no longer want any. They are not the same ending.

for a middle

Explain which callbacks fire: a completion handler does not run for a cancelled run, while a hook attached to any ending runs for both and cannot tell them apart on its own.

for a senior

Show the correctness consequence: commits, cursor advances and retries placed on an any-ending hook record abandoned work as finished every time a user closes a screen.

for a principal

Set the rule that abandonment is a third outcome alongside success and failure, so reporting, retry policy and commit points all stop collapsing it into one of the other two.

## Two endings that look alike and are not From outside, both endings look identical: values stop arriving. Inside the subscription they are opposites. - **Completion** is a **terminal signal** produced by the source and delivered downstream. It means *there is nothing more to produce, and nothing went wrong*. It arrives exactly once and ends the sequence for the subscriber. - **Cancellation** is a **request** produced by the subscriber and delivered upstream. It means *I no longer want values*. The subscriber is not owed a signal in return; it stops hearing because it asked to. So the honest answer to "how can the code tell" is: **by which path fired, not by observing the silence.** Silence is common to both, and to a source that is simply slow. | | Completed | Cancelled | |---|---|---| | Who initiated it | The source | The subscriber | | Direction | Downstream signal | Upstream request | | Completion handler runs | Yes | No | | Any-ending teardown hook runs | Yes | Yes | | Meaning | The work finished | The work was abandoned | ## Why the distinction has teeth An ending is usually the moment code commits something, and committing the wrong thing is a correctness bug, not a cosmetic one. - **Treating a cancel as success.** The conversation is marked fully synchronised, a batch is committed as delivered, a cursor is advanced past messages that were never handled. The user closed the screen mid-stream and the system recorded that everything arrived. - **Treating a cancel as failure.** The other error: a retry fires, or a failure path runs, for a run that nobody wanted any more. At best it wastes the work again; at worst it re-opens the resource the cancel was supposed to release. - **Treating a cancel as still running.** Code that waits on the subscription for a terminal signal that will never come waits forever. The correct default is that **cancellation is neither success nor failure** — it is abandonment, and most decisions that hang off an ending should simply not be taken. ## How to make it knowable Since the contract gives the subscriber no cancellation signal, the code must record the fact where it is known: 1. **At the cancel site.** The owner that cancels knows it did; it can set the reason before cancelling, so the any-ending hook reads it rather than guessing. 2. **In the hook's parameter.** Where the mechanism offers an ending reason to the any-ending hook, use it, and branch only where the decision genuinely depends on the ending. 3. **By keeping commits out of teardown.** A commit placed on the completion path alone is automatically correct: it cannot fire on a cancel, because the completion handler never runs for a cancelled run. The third is the cheapest and most reliable. Put release logic in the any-ending hook and meaning-bearing logic on the specific ending that licenses it. ## The trap in review The defect shape is specific and easy to spot once you know it: a single block of code doing both release and commit, attached to whatever hook fires on every ending. It reads like tidy cleanup, it passes tests (which always let the sequence complete), and in production it marks abandoned work as finished every time a user closes a screen. Splitting that block — release unconditionally, commit only on completion — is usually a two-line change and removes the whole class. Stated for an interview: completion is a signal you receive, cancellation is a request you sent; only the first tells you the work is done, so never let a hook that fires on both decide that it was.

  • Can a subscriber be given both a cancellation and a completion for the same run?
    Only as a race, and only one wins. The subscriber may cancel at the instant a completion is travelling down; the completion may already be delivered, or it may arrive at a subscriber that has stopped listening and be dropped. Teardown must therefore be safe to reach from either path, once.
  • A pipeline waits for the run to end before releasing a shared lock. What breaks?
    If it waits for a completion signal, a cancelled run never delivers one and the lock is held forever. Release on any ending instead, and keep only the ending-dependent decision on the completion path.

saying these in an interview costs you the question

  • Says a completion handler also runs when the subscriber cancels
  • Treats cancellation as a failure and retries the work
  • Marks work as finished from a hook that fires on every ending
  • Infers which ending occurred from the fact that values stopped
  • Expects the source to signal back that the cancel took effect