skip to content

A hand-written stream source emits a failure signal and then keeps pushing values — which rule does that break?

level: juniorimportance: must knowfreq 70%

answer

  1. the sequence has a grammar
  2. endings are signals too
  3. how many endings are allowed
  4. what may follow the ending
  5. consumer releases state at the ending

basics

~20 s

The signal grammar: a run carries any number of value signals and then at most one terminal signal, completion or failure, never both. A failure is that ending, not another value, so the source must go silent after it.

solid answer

~50 s

A push-based source speaks a fixed grammar: zero or more **value** signals, then **at most one terminal** signal — either completion or failure — and nothing at all afterwards. The adapter treats the failure as if it were a warning and keeps draining its buffer, but the failure already ended the sequence. The consumer is entitled to release its per-run state the moment the terminal arrives, so anything delivered later hits a stage that has already torn itself down, or is silently dropped by a defensive stage and the data loss never surfaces. Note what the rule does not say: it caps terminals at one, it does not require one, so a source that runs forever is fine. The fix is a latched flag at the emission boundary that turns every later signal into a no-op.

code

pseudocode · 9 lines
pseudocode
function runSource(consumer):
    try:
        for each row in rows:
            consumer.value(row)
        consumer.completed()
    catch err:
        consumer.failure(err)          // the sequence ends here
        for each row in leftovers:     // defect: it already ended
            consumer.value(row)

go deeper

for a junior

Recall the shape of a run: any number of values, then at most one ending, and silence after it. Know that a failure is an ending rather than a strange value.

for a middle

Explain why the prohibition sits on the source: the consumer releases per-run state at the terminal, so a later signal reaches a stage that has torn itself down or is dropped invisibly.

for a senior

Show how you would enforce it in code you accept from other teams — one emission boundary, a latched flag, no error path that can reach the consumer after another path ended the run.

for a principal

Frame it as an interoperability guarantee: stages written by different people compose only because the ending is agreed, and every locally convenient exception to it pushes cost into every consumer.

## The two kinds of signal A push-based source talks to a consumer with exactly two kinds of signal. A **value signal** carries one element of the sequence. A **terminal signal** says the sequence is over, and comes in two flavours: **completion**, meaning the source produced everything it had, and **failure**, meaning the source cannot continue and is handing over a reason instead of a value. The whole grammar is one line: *any number of value signals, then at most one terminal signal, and nothing after it.* As a pattern, `value* (completion | failure)?`. Three rules fall straight out of it: - A run carries **at most one** terminal signal — not one of each kind, not two completions. - A failure **is** the ending, not an element the consumer reads past. - After the terminal signal the source is **silent on that subscription**, permanently. | signal kind | how many per run | may anything follow it | |---|---|---| | value | zero or more | yes — more values, or the terminal | | completion | at most one | no | | failure | at most one | no | ## Why the reviewed adapter is broken The adapter catches an error, reports it as a failure, and then drains whatever it had already buffered. Read as a protocol rather than as a pile of callbacks, that is two messages after a goodbye. Four things go wrong, none of them at the line that caused them: - The consumer may have **released its per-run state** at the failure — an accumulator, a handle it was holding, an open resource. A later value arrives at a stage that no longer has anywhere to put it. - A stage that follows the contract defensively **drops** the stray signals. That is the worse outcome, because the violation is now invisible: values vanish and nothing reports it. - If the leftovers are pushed from the worker that was unwinding the error, an exception thrown downstream surfaces **inside the source's own error path**, where nobody is looking for it. - The symptom appears far from the cause, usually in whichever stage happened to hold state, which is why this defect is expensive to find and cheap to prevent. ## What the consumer is entitled to do at the ending The grammar is what makes a consumer's job finite. On the terminal signal a consumer may: 1. Emit whatever it had accumulated and forget it — a stage that counts, batches or folds knows it will never be asked to fold again. 2. Release everything tied to this run: buffers, timers, the handle it was given when it attached. 3. Treat any later signal as a defect in the source rather than as data, because the contract says one cannot legitimately arrive. That third point is the reason the rule is stated as a prohibition on the source rather than as advice to the consumer. If a consumer had to stay ready for stragglers, it could never release anything, and the ending would carry no information. ## Exactly-one is required; ending at all is not The two halves of the rule are often collapsed and they are different. A source is **forbidden** to deliver a second terminal, and **not obliged** to deliver a first one: a source over a live feed may run for the lifetime of the process. That has a practical consequence — a consumer that needs an ending has to impose one itself, and from the outside a source that will never end is indistinguishable from one that has stalled, except by waiting a chosen amount of time and deciding. One related case is worth naming because it comes up in exactly this review. If the source finishes cleanly, signals completion, and *then* its own cleanup step fails, it may not signal that failure: the sequence has ended and accepts nothing more. The failure has to be reported through some channel outside the stream. Trying to squeeze it into the ended sequence is the same violation wearing a sympathetic motive. ## Reading it as a review checklist For a hand-written source, four questions settle conformance on this rule alone: - Is there **one** place where signals leave the source, or several scattered through the code? - Does that place carry a **latched flag** that is set by the first terminal and consulted by every later signal? - Can any error path reach the consumer **after** another path has already ended the run? - Does the failure signal carry a **reason**, rather than being a completion with a log line beside it? A source that answers those four cleanly cannot break this rule, whatever else it gets wrong.

  • Is a source that never emits a terminal signal violating this contract?
    No. The grammar caps terminal signals at one; it does not require one. A source over a continuing feed may run as long as the process does. A consumer that needs an ending must impose it itself, and from outside, endless and stalled look identical until you pick a waiting time.
  • May one run deliver both a completion and a failure?
    No — at most one terminal signal of either kind, whichever the source reaches first. If a source completes and then its cleanup fails, the sequence has already ended and cannot carry the failure; it has to be reported outside the stream, not pushed into a run that is over.

A sequence is a letter: any number of lines, then one sign-off. A line added after the sign-off does not extend the letter — it is a second letter nobody agreed to read.

saying these in an interview costs you the question

  • Treats a failure as a value the consumer can keep reading past
  • Says a source may complete after it has already failed
  • Thinks stray signals are harmless because consumers drop them
  • Assumes every stream must eventually reach an ending
  • Leaves cleanup waiting for more values after the terminal signal
  • Signals a post-completion cleanup error into the ended sequence