Build a gRPC-style RPC framework (14 scenes)
Scene 6.5 · Cancellation: an event, not a clock
A deadline fires from a clock; cancellation fires from an event. Both ride one Context object down the chain, so aborting a parent stops all the doomed downstream work.
Previously
The shrinking budget bar stopped zombie work when the clock ran out — but when the user closes the tab at 200ms no clock has expired, so we need a second trigger that says 'stop now' and rides the exact same propagation path.
Scene 6.5
Cancellation: an event, not a clock
Diagram
The same A→B→C chain carrying greet("Ada"), now with a healthy budget bar — the deadline is NOT about to fire. The new control fires a stop EVENT at a chosen moment (the dashed RST_STREAM mark on the bar). The CONTEXT chip under every arrow bundles {deadline, cancel} as one travelling unit; when the cancel reaches a hop its pill flips active→cancelled and that node early-returns. A node still marked 'active' deep in the chain is doing work for a caller who already left.
cancellation: a 'never mind' EVENT, not a clock
Context chip carries {deadline, cancel} as one unit →
The chain is running greet("Ada") with a 10-second deadline — nearly all of it still on the clock. Earlier we learned a *deadline* propagates the remaining time so a slow downstream stops wasting effort once *time runs out*. But time hasn't run out here. Instead, the client decides to quit — imagine the user closing the tab. That decision is not a clock reading; it's a one-off event. gRPC reuses the RST_STREAM frame from the transport layer to signal it: the first thing the chain learns is that the caller said 'never mind'. We call this stop-now signal **cancellation** — a trigger that fires from an event rather than from elapsed time. Watch the cancel at 200ms ripple down A→B→C, flipping every hop from active to cancelled so all three servers abort their work mid-computation.
Implementation
Client.cancel
the caller quits — an event, no clock involved
1def cancel(call):2 # fired by an EVENT (tab closed), not by elapsed time3 call.stream.send(RST_STREAM) # HTTP/2 cancel frame4 call.ctx.cancel(reason = CANCELLED)
Server.handle
each hop watches its Context and early-returns on cancel
1def handle(req, ctx):2 for step in work(req):3 if ctx.cancelled: # observed, not polled-on-a-clock4 return # abort mid-flight; nothing to reply5 partial = compute(step)6 # the downstream call inherits THIS ctx7 child = stub.call(req, ctx = ctx)8 return assemble(partial)
Context.cancel
one object carries {deadline, cancel}; it fans out to children
1def cancel(self, reason):2 self.cancelled = True3 # same path a deadline-expiry would walk4 for child in self.children:5 child.cancel(reason) # propagate down A->B->C
Not sure what to ask? Tap a question — the staff engineer answers in the chat panel.