What is a cancellation checkpoint, and how would you make a 30-second CPU-bound computation loop cancellable? What determines how quickly cancellation actually takes effect?
answer
- checkpoint = observe signal + safe to unwind
- await/blocking calls = free checkpoints
- tight loop: poll every N iterations
- latency = gap between checks + cleanup
- never checkpoint mid-critical-section
basics
~20 sA checkpoint is a point where the task tests the cancellation signal and abandons the work if it is set. Blocking and awaiting operations usually provide them implicitly; a pure compute loop has none, so you poll the signal every N iterations. Cancellation latency is roughly the time between checkpoints plus cleanup time.
solid answer
~60 sA **cancellation checkpoint** is a place in the task where it observes the signal and, if set, unwinds. Checkpoints must sit where the task's invariants hold, so abandoning is safe. Most checkpoints come for free: in async runtimes every suspension point is one, and interruption-aware blocking calls check on entry and abort while blocked. A tight numeric loop has none of these — it never yields, never blocks — so it is uncancellable until you add a poll: ``` for i in 0..N: if i % 4096 == 0 and cancelled(): raise Cancelled step(i) ``` The check must be cheap (a plain/relaxed read of a flag, never a lock), and placed in the loop that actually accumulates time — the outer loop if the inner body is short. **Latency** ≈ worst-case work between two checkpoints + time to unwind and clean up. That is what you tune: check too rarely and shutdown misses its grace period; check every iteration and you pay measurable overhead in a hot loop. Chunking work into bounded units gives you a natural checkpoint boundary.
code
text · 12 lines# BAD: 30 s of work, one checkpoint at the start
if cancelled(): return
run_30_seconds() # latency: 30 s
# GOOD: chunked, checkpoint between units
for chunk in chunks(work, size=10_000):
if cancelled(): raise Cancelled # latency ~ one chunk (~20 ms)
process(chunk) # invariants hold between chunks
# Timeline
# signal set --|-------- up to one chunk --------|-- unwind+cleanup --| task ended
# ^ cancel() ^ checkpoint observedgo deeper
Define a checkpoint, note that awaiting/blocking calls usually provide them, and show the every-N-iterations poll for a compute loop.
Explain placement (outer loop, between units of work, never mid-invariant), the cost of checking too often, and the latency formula including cleanup.
Reason from a budget: derive the checkpoint interval from the shutdown grace period or client timeout, discuss how one un-checkpointed leaf pins an entire subtree, and how you test cancellability rather than assuming it.
Treat cancellability as a service-wide property: audit for un-checkpointed spans and third-party latency floors, set a per-task latency SLO for shutdown, and design work as bounded resumable chunks so checkpoints and progress persistence fall out of the same structure.
## Definition A **cancellation checkpoint** (also called a cancellation point, suspension point, or safepoint for cancellation) is a location in a task's execution where two things are true: 1. the task **reads the cancellation signal**, and 2. **abandoning right there is safe** — the task's own invariants hold, and unwinding from this point will run cleanup correctly. Cooperative cancellation is exactly the discipline of having enough of these, in the right places. ## Where checkpoints come from **Implicit, from the runtime.** In coroutine/async models, every point where the task can suspend is naturally a checkpoint: the runtime already has control there, so it can resume the task with a cancellation instead of a value. That is why async code often "just works" with cancellation as long as it awaits something regularly. **Implicit, from blocking APIs.** Interruption-aware blocking calls typically check the signal on entry and abort with an interrupted status if the signal arrives while parked. Timed variants ("wait up to 200 ms") give you a bounded checkpoint interval even if the underlying operation itself is not interruptible. **Explicit, written by you.** Pure computation — matrix work, parsing a big buffer, simulation, compression — never blocks and never yields, so the runtime gets no opportunity to intervene. Here you must poll. ## Making a CPU-bound loop cancellable The standard shape: ``` CHECK_EVERY = 4096 for i in 0..N: if i % CHECK_EVERY == 0 and cancelled(): cleanup_partial() raise Cancelled step(i) ``` Design points: - **Make the read cheap.** The signal should be a simple flag readable without synchronisation stronger than an ordinary atomic/volatile-style read. Taking a lock, allocating, or calling into a registry per iteration turns cancellation support into a performance bug. Reading a stale value briefly is fine — it only delays the stop by microseconds. - **Amortise the check.** Testing every 4096 iterations makes the check's cost negligible while keeping latency in the sub-millisecond to millisecond range for typical iteration costs. The counter-and-mask trick avoids even the branch cost being significant. - **Put it in the loop that owns the time.** If the inner loop body is nanoseconds and the outer loop runs 10,000 times over 30 seconds, check in the outer loop. Checking in the innermost loop of a hot kernel can defeat vectorisation and cost real throughput. - **Do not check inside a critical section that must complete.** A checkpoint in the middle of a two-step state update violates rule (2) above: you would abandon with the invariant broken. Put checkpoints at the boundaries between units of work, not inside them. - **Prefer chunking.** Restructure the work as a sequence of bounded units ("process 10,000 rows", "one tile of the image"), with the checkpoint between units. This also gives you natural progress reporting and a place to persist partial state if the work is resumable. ## Cancellation latency The useful formula: **latency ≈ (worst-case time between consecutive checkpoints) + (time to unwind and run cleanup)** Both halves matter. A loop that checks every 100 ms but whose cleanup flushes a 2-second write is a 2-second cancellation. Latency composes down a task tree, too: a parent cannot finish cancelling until its slowest descendant has passed a checkpoint and cleaned up, so the tree's latency is dominated by its worst path — a single un-checkpointed leaf pins the whole subtree. You size the interval against a **budget**, not a feeling. If shutdown grants 10 seconds of grace and you want three retries of escalation inside it, per-task cancellation must land in well under a second; if the caller's timeout is 200 ms and you want the work to actually stop when the client disconnects, checkpoints need to be tens of milliseconds apart. ## Anti-patterns - **A loop with no checkpoint at all**, then blaming the framework for "cancellation not working". The framework can only deliver the signal; observing it is the task's job. - **Checking only before the loop.** The signal almost always arrives *during* the work. - **Checking, then continuing anyway** ("log it and carry on"), which converts cancellation into a warning message. - **A hidden un-checkpointed span inside a library call** — a third-party parse or compress step that runs for seconds. Wrap such calls with a smaller unit of input if you can; otherwise document the latency floor they impose. - **Cleanup with no bound.** The second half of the latency formula deserves its own budget. ## Testing it Cancellability is testable: start the task, cancel after a fixed delay, and assert both that the task terminated within the latency budget and that its resources were released. A test that only asserts "cancel() returned" proves nothing, because the signal always returns immediately regardless of whether anyone is listening.
- How do you choose how often to poll the cancellation flag?Work backwards from a budget: the caller's timeout or the shutdown grace period sets the maximum acceptable latency, and the checkpoint interval must be a fraction of that after subtracting cleanup time. Then measure the overhead — pick the largest interval that meets the budget so the check stays in the noise, typically a few thousand iterations or a chunk of a few milliseconds to tens of milliseconds of work.
- A library call in the middle of your loop blocks for several seconds and ignores cancellation. What are your options?Either feed it smaller inputs so each call is short and put checkpoints between calls, or use a variant that accepts a timeout so it returns control periodically. If neither is possible, isolate it — run it where you can close its underlying resource to force it to fail, or in a separate process you can terminate — and document the latency floor it imposes on every enclosing scope.
saying these in an interview costs you the question
- Writes a compute loop with no checkpoint and expects cancellation to work
- Checks the signal only once before starting the work
- Takes a lock or allocates on every iteration just to read the cancellation state
- Places the checkpoint inside a multi-step update, so cancelling leaves broken invariants
- Ignores cleanup time when reasoning about how fast cancellation takes effect