skip to content

Which work in an ingest pipeline justifies a single-worker execution context, and what does serialising it cost?

level: seniorimportance: should knowfreq 40%

answer

  1. one consumer, one queue
  2. ordering or an exclusive resource
  3. mutual exclusion without a lock
  4. throughput ceiling is one worker
  5. guarantee dies if access escapes

basics

~20 s

Work that must happen one at a time earns it: assigning sequence numbers, or driving a resource only one thread may touch. A single serial worker gives exclusion and queue order without a lock, at the cost of a one-worker ceiling and head-of-line delay.

solid answer

~50 s

A single-worker context is a queue with exactly one consumer: each task runs to completion before the next begins. Two workloads in an ingest pipeline earn that — stamping each derived file with the next sequence number, which must be handed out one at a time, and driving a handle that is not safe for concurrent use. Both get mutual exclusion and in-queue ordering without anyone writing a lock. The costs are real and worth saying out loud: throughput is capped at one worker, a slow task delays everything queued behind it, the queue grows silently unless it is bounded and watched, and the guarantee holds only while *every* access goes through that context — one caller reaching the resource from elsewhere voids it. Note what it cannot do: it preserves the order tasks were queued in, it cannot restore an order already lost upstream.

code

pseudocode · 11 lines
pseudocode
next_sequence = 0

pipeline = decoded_files()
    .on_context(SERIAL)                    // exactly one worker drains this queue
    .map(file -> {
        next_sequence = next_sequence + 1  // safe: only this worker ever runs here
        return stamp(file, next_sequence)
    })
    .subscribe(store)

// the guarantee ends the moment any other stage touches next_sequence

go deeper

for a junior

Know that some work must happen one item at a time, and that a pipeline can dedicate a single worker to it instead of taking a lock.

for a middle

Explain that a one-worker context is a queue with a single consumer, giving exclusion and order of service, and that its throughput ceiling is that one worker.

for a senior

Point at the failures you would look for: a growing queue on that context, a slow task delaying unrelated work behind it, or a second path reaching the state around the worker.

for a principal

Decide whether serial work gets a dedicated worker per resource or one shared serial context, and own what that means for isolation between teams and for total threads.

## What one worker actually guarantees A single-worker execution context is a queue drained by exactly one worker. Everything else follows from that one fact: - **Mutual exclusion.** Two tasks on that context never run at the same time, so state touched only from there needs no lock. - **Order of service.** Tasks run in the order they were placed on the queue. - **A single thread of access.** Anything requiring that the same thread touch it every time gets that, because there is only one. It is a synchronisation mechanism wearing scheduling clothes. Where a lock makes the caller wait its turn, a serial context makes the *work* wait its turn and lets the caller move on. ## Two workloads in an ingest pipeline that earn it 1. **Assigning sequence numbers.** Each derived file must be stamped with the next number in an unbroken sequence. Hand that to two workers concurrently and two files can read the same value, or the sequence can develop holes. A single worker hands them out one at a time by construction. 2. **A resource that is not safe for concurrent use.** An index writer, a handle that must be opened, appended to and closed by one thread, an encoder holding internal state between calls. Rather than guarding every call site, route all of that work onto one worker and the constraint is satisfied structurally. ## What serialising costs | Cost | What it looks like | |---|---| | Throughput ceiling | whatever one worker can process per second, and no more, however many processors the host has | | Head-of-line blocking | one slow task delays every task queued behind it, including unrelated work sharing that context | | Hidden backlog | the queue absorbs an arrival rate above the service rate silently, converting overload into latency and memory rather than an error | | Coupling through the queue | two workloads placed on the same serial worker now share a latency fate they did not choose | The first two are inherent; the second two are choices. A bounded queue and a published queue-depth metric turn an invisible backlog into a signal, and giving each ordered workload its own single-worker context removes the coupling at the cost of one more thread. ## How the guarantee is voided The exclusivity is a property of the *access pattern*, not of the context. It holds only while every path that touches the state goes through that one worker. Three ways it is commonly lost: - a helper method that touches the same state is called from a different stage, because it looked like ordinary code; - the state is also read from elsewhere for a metric or a health check, and an unsynchronised read of state mutated on another worker is still unsafe; - the workload is later split for throughput across two serial contexts, which is two workers and therefore no exclusion at all. When the guarantee is structural rather than enforced, state the invariant next to the state itself, so that the next reader knows why no lock is present. ## Serial worker or lock? Both give exclusion; they differ in **who waits**. A lock parks the calling worker until it wins, which is costly when that caller is one of a handful of workers on a small compute pool. A serial context puts the task in a queue and releases the caller immediately, which keeps the caller's context free — but the result is no longer available at the point of the call, so the code around it must be written to continue asynchronously. Choose the lock when the critical section is tiny and the caller has nothing else to do; choose the serial context when the protected work is substantial or the callers are workers you cannot afford to park. ## What to demonstrate in an interview Name the workload that genuinely requires one-at-a-time execution, say which of the two reasons applies — ordering or an exclusive resource — and then volunteer the costs without being asked. Adding that the queue needs a bound and a depth metric, and that a second access path voids the whole arrangement, is what separates someone who has operated such a pipeline from someone who has read about it.

  • Two independent ordered workloads each need serialising. Should they share one serial context?
    Only if you accept that each may delay the other. One worker drains one queue, so a slow task in the first workload pushes out the latency of the second even though they share no state. Give each its own single-worker context when their latency goals differ or either can be slow; share one when both are consistently short and total thread count matters more to you than isolation between them.
  • What does a single-worker context give you that a lock around the same resource does not?
    It moves the work instead of the waiting. A lock parks the calling worker until it wins, so a scarce non-blocking worker sits idle doing nothing; a serial context enqueues the task and releases the caller immediately. The price is that the result is no longer available where the call was made, so the surrounding code must be written to continue asynchronously.

A single serial worker is the one clerk at a counter: the queue guarantees people are served in the order they joined it, and one customer with a complicated request delays everyone standing behind them.

saying these in an interview costs you the question

  • A serial worker makes the whole pipeline thread-safe
  • Serialising costs nothing because the tasks are short anyway
  • A serial worker restores an order already lost upstream
  • One serial context can safely host every ordered workload in the process
  • A single worker removes any need to bound its queue