flare_mlake · API · http

flare.http.cancel

Cancel token plumbed from the reactor into request handlers.

A handler runs to completion regardless of client behaviour. If the client TCP-disconnects mid-handler, the handler has no signal until the next _send returns EPIPE. By then the handler has already done its expensive work (DB query, downstream HTTP fan-out, JSON serialisation).

the reactor allocates one CancelCell per connection (heap-allocated Int). Cancel is a thin handle the reactor passes to a CancelHandler.serve(req, cancel); the handler polls cancel.cancelled() between expensive steps and short-circuits when True.

Cancellation is cooperative, not preemptive — Mojo can't preempt synchronous code, and synchronous preemption would defeat the reactor's per-thread invariant anyway. The contract is: the handler checks the flag at boundaries it owns; the reactor sets it at one of:

  • CancelReason.PEER_CLOSED — recv == 0 (peer FIN) observed before the response was queued.
  • CancelReason.TIMEOUT — a per-request, per-handler, or per-body-read deadline fired.
  • CancelReason.SHUTDOWN — HttpServer.drain(timeout_ms) was called.

The default-initialised Cancel returned by Cancel.never() is a sentinel that never fires; tests and synthetic CancelHandler calls that don't have a real reactor cell behind them use it.

Implementation

A CancelCell heap-allocates a single Int and owns its lifetime. Cancel carries the cell's address as an Int and rebuilds a fresh Pointer[Int, MutUntrackedOrigin] per access — the same pattern the multicore Scheduler uses for the stopping flag, and the only one that survives Mojo's origin / aliasing model when passing the cancel handle across function-call boundaries. Reads / writes go through Atomic[Int64] acquire-load / release-store (the cell can be flipped from a peer thread on shutdown), which lowers to a plain mov on x86-64 (TSO) and to ldar / stlr on ARM64:

  • Storing a typed Pointer[Int, MutUntrackedOrigin] as a struct field in Cancel produces stale reads after the struct is passed through a function (verified empirically: reads at a numerically-correct address returned the pointer struct size, 8, instead of the live byte).
  • An inline UInt8 cell on the owning struct (instead of heap) does not have a stable address across the synchronous call chain the reactor builds, again per direct testing.

The combination here — heap allocation for cell stability + Int address in Cancel for transport stability — is the only one that passes the test suite and the cell.flip(reason); h.serve(req^, cell.handle()) round-trip.

Why Int instead of UInt8 for the cell value: a byte-sized heap cell hit the same aliasing failure as the typed-pointer field, suggesting Mojo's aliasing model treats sub-word-aligned loads through MutUntrackedOrigin differently than full-word loads. A machine word avoids the path the bug appeared to depend on. The cell is one cache line per connection — acceptable cost for the cancel infrastructure.

Structs

CancelReason

struct CancelReason

Reason the cancel cell was flipped.

Stored in the cell as an Int; NONE (zero) means "still live." The handler can branch on the reason via cancel.reason() == CancelReason.X when it wants to log or take a different short-circuit path depending on why it was cancelled (for example, returning a 503 on shutdown vs. a 408 on timeout).

Implements Movable

comptime NONE = Int(0)

Cell is still live; cancel.cancelled() returns False.

comptime PEER_CLOSED = Int(1)

Peer sent FIN (recv == 0) before the response was queued.

comptime TIMEOUT = Int(2)

A deadline expired (per-request, per-handler, or per-body-read).

comptime SHUTDOWN = Int(3)

HttpServer.drain(timeout_ms) was called.

CancelCell

struct CancelCell

Heap-allocated per-connection cancel cell.

Owned by the reactor for the lifetime of a connection (the reactor keeps one of these on each ConnHandle). Cancel handles point at this cell. Reset to CancelReason.NONE between pipelined requests so a peer-FIN on one request doesn't bleed into the next.

Implements Movable

__init__

def __init__(out self)

Allocate a fresh cell initialised to NONE.

__deinit__

def __deinit__(deinit self)

flip

def flip(mut self, reason: Int)

Set the cell's reason with a release store.

Pairs with the acquire load in Cancel.cancelled / Cancel.reason so a reader on another thread that observes the flip also observes every write the flipper made before it.

reset

def reset(mut self)

Reset the cell to NONE with a release store.

handle

def handle(mut self) -> Cancel

Hand out a Cancel value bound to this cell.

Cancel

struct Cancel

A handle to a per-request cancel cell owned by the reactor.

Passed to CancelHandler.serve(req, cancel) by the reactor. Value-copy semantics: copies share the underlying cell.

A handler that ignores cancellation is correct (the request shape is unchanged), but the reactor still flips the cell and the body the handler returns will be the result of work the reactor knew the client had hung up on.

Example:

from flare.http import CancelHandler, Cancel, Request, Response, ok

@fieldwise_init
struct SlowHandler(CancelHandler, Copyable):
    def serve(self, req: Request, cancel: Cancel) raises -> Response:
        for i in range(100):
            if cancel.cancelled():
                return ok("partial: " + String(i))
            # ... one expensive step ...
        return ok("done")

Implements Copyable, ImplicitlyCopyable, Movable

__init__

def __init__(out self, addr: Int)

never

def never() -> Self

Return a sentinel Cancel whose cancelled() is always False.

cancelled

def cancelled(self) -> Bool

Return True once the cell is non-zero (acquire load).

reason

def reason(self) -> Int

Return the reason code currently in the cell (acquire load).

addr

def addr(self) -> Int

Return the raw address of the backing cell (0 for the never() sentinel). Used by the deadline watchdog to flip this cell from another thread.