skip to content

A message source opens a connection when subscribed — on which endings must its teardown run, and which ending is forgotten?

level: middleimportance: should knowfreq 50%

answer

  1. three ways a run can end
  2. completed, failed, cancelled
  3. two arrive down, one goes up
  4. one hook for any ending
  5. guard it so it runs once

basics

~20 s

A subscription ends in one of three ways: normal completion, failure, or cancellation. Teardown must run on all three, and cancellation is the one teams forget, because it is the only ending the source did not choose.

solid answer

~50 s

Whatever a source acquires when a subscriber attaches — a connection, a timer, a buffer, a registration with a live feed — has to be released when that subscription ends, and there are three ways it can end: the source completes, the source fails, or the subscriber cancels. A hook attached to completion alone leaves the resource open on the other two paths, and cancellation is the path most often missed because it is the only ending the source did not initiate. The reliable shape is a single any-ending hook that releases the resource, guarded so it runs **once**: cancellation can race a terminal signal, and both may try to fire it. Registering the hook per subscription, not per pipeline description, matters too — each run acquires its own resource and must release its own.

code

pseudocode · 15 lines
pseudocode
function subscribeWithCleanup(source, subscriber):
    connection = openConnection()
    alreadyTornDown = false

    function finish():
        if alreadyTornDown: return      // cancel can race a terminal signal
        alreadyTornDown = true
        connection.close()

    subscriber.onCancel(finish)

    source.run(connection,
        onValue    = function(v) { subscriber.emit(v) },
        onComplete = function()  { finish(); subscriber.complete() },
        onFailure  = function(e) { finish(); subscriber.fail(e) })

go deeper

for a junior

Remember that a run can end three ways — it finished, it failed, or someone cancelled it — and that anything opened must be released on all three, not just the happy one.

for a middle

Explain why cancellation is the missed path: it is the only ending the source did not initiate, and it arrives from the opposite direction to a terminal signal.

for a senior

Show the production shape: one any-ending hook, guarded to run once because cancel can race a terminal signal, owned by the subscription rather than the description.

for a principal

Make resource ownership per subscription a reviewable rule, so that a connection count that never returns to zero is a caught defect rather than a production incident.

## Three endings, one teardown A subscription that acquired something must release it, and the question is only *on which paths*. There are three ways a subscription reaches its end: 1. **Completion** — the source has no more values and signals that it is finished normally. 2. **Failure** — the source signals an error and the sequence ends abnormally. 3. **Cancellation** — the subscriber asked to stop before either of the above. The first two are **terminal signals travelling downstream**; the third is a **request travelling upstream**. They arrive by different routes, and that asymmetry is the whole reason teardown gets written incorrectly: a hook wired to the arrival of a terminal signal simply never sees the third path. | Ending | Who initiates it | Terminal signal delivered | Commonly handled | |---|---|---|---| | Completion | The source | Yes | Almost always | | Failure | The source | Yes | Usually | | Cancellation | The subscriber | No | Frequently missed | ## Why cancellation is the forgotten one Every other ending is something the source *did*, so the code that produces the ending is right next to the code that could clean up. Cancellation is something that happens **to** the source, from outside, at a moment it did not choose — often while it is blocked waiting for the next value. It is also the ending that is hardest to see in testing: a test subscribes, reads the values, and lets the sequence finish. Nobody closes the screen halfway. The path with no cleanup is therefore also the path with no coverage, and it only shows up in production, where users close screens constantly. ## The shape that works The reliable pattern is one **any-ending hook**, registered when the subscription is established, that releases everything that subscription acquired. Three properties make it correct: - **It covers all three paths.** It is invoked by completion, by failure and by cancellation, rather than being attached to one of them. - **It runs once.** Cancellation can arrive at the same moment a terminal signal is travelling down, so both may attempt teardown. A flag set on first entry makes the second attempt a no-op. Without it, a connection is closed twice or a count is decremented twice. - **It belongs to the subscription, not to the description.** The pipeline description is inert and holds nothing; each subscription acquires its own resource and must release its own. A hook stored on the shared description would release the wrong run's resource. A fourth point is worth stating because it is a real edge: if acquisition itself fails partway — the connection opened but the registration after it threw — the partially acquired state still has to be released, so the hook is registered as each piece is acquired rather than only after the last one. ## What belongs in the hook, and what does not The hook exists to **release**, and keeping it to that keeps it safe: - Close what was opened, cancel what was armed, drop what was buffered, deregister what was registered. - Keep it non-throwing: a teardown that throws during a failure ending can lose the original failure. - Keep it fast: it may run while the owner is being destroyed, and blocking there stalls the cancel travelling further upstream. What does not belong is business meaning. Committing state, marking work as delivered, or deciding to retry depends on *which* ending occurred, and an any-ending hook by itself cannot tell completion from cancellation. Release unconditionally in the hook; take decisions that depend on the ending where the ending is known. ## How to answer it Name the three endings, say that teardown must be on all three, and then name cancellation as the one that gets missed along with the reason — it is the ending the source did not choose and the one tests rarely produce. Adding the once-only guard, and the fact that the hook belongs to the subscription rather than to the pipeline description, is what separates a candidate who has read about this from one who has debugged a connection count that never came back down.

  • Why does the teardown hook need a once-only guard at all?
    Because the endings can race. A cancel travelling upstream may arrive at the same moment a terminal signal is travelling down, so both paths reach teardown. Releasing twice closes an already-closed connection or double-decrements a count, which produces failures that look unrelated to the subscription.
  • Can the same teardown hook decide whether to retry the work?
    No. A hook that fires on any ending cannot tell completion from cancellation, and retrying after a user closed the screen is exactly wrong. Keep the hook to releasing resources, and take ending-dependent decisions where the ending is actually known.

saying these in an interview costs you the question

  • Attaches cleanup only to the completion path
  • Says cancellation needs no cleanup because the subscriber is gone
  • Assumes teardown can never be reached twice
  • Stores the acquired resource on the shared pipeline description
  • Puts retry or commit decisions inside the any-ending hook