Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ticks and timers

A stage that needs time rather than traffic to drive it implements tick_interval and on_tick. The host asks once, at the end of construction, how often the stage wants calling, and then calls it on that schedule for as long as the pipeline lives, whether or not bytes are moving. Without it a stalled stream and a finished one are indistinguishable from inside a plugin.

ctx.input() is empty during a tick, so there is nothing to pass through: anything emitted is forwarded, and it continues downstream through the stages below this one, the same way on_eof output cascades. The stages above are upstream of a chunk that did not come from them. Emitting nothing is the common case and costs nothing. Whatever is emitted is one unit unless ctx.boundary() says otherwise, exactly as in on_bytes.

tick_interval is read exactly once, so it must not depend on anything that changes later. A stage whose interval is configurable reads its config in build and answers from that. A zero period reads as “no ticks”, which is the same answer as None and is what a stage configured with an interval of zero means by it.

Cadence, not delay

The host owns the clock, because a guest cannot reach one. What a stage gets is therefore a cadence rather than a delay: a tick that came due while bytes were flowing fires at the next opportunity, which can be immediately after the bytes it is about arrived.

A stage that means “an interval after I started waiting” says so with ctx.rearm(), which restarts its own schedule from now. It is cheap and harmless to call often: it sets a flag the host reads once at the end of the call, and it is ignored for a stage that asked for no ticks. A tick may rearm too, which is how a stage that has just given up waiting says so.

That is how block turns flush into a latency bound it can actually promise: it rearms when a block starts filling, so the interval is measured from the first byte held rather than from wherever a shared cadence happened to be.

StageWantsUses
rateSamples on a fixed cadence, so a stalled stream is reportedthe tick
blockA bound on how long a byte waits, measured from that byterearm
timeoutA deadline measured from the last byte, to a known accuracyboth

What it costs

The pipeline owns the schedules, one deadline per ticking stage, and the host owns a single timer that asks whether any of them are due. The timer runs at the shortest period any stage in that segment asked for; a stage that wanted a longer one is simply not due on most wakeups, which is cheaper than a timer each. A segment with nothing ticking builds no timer at all and awaits its read directly.

So the cost is one timer per segment per direction per connection, which multiplies under fork: a stage asking for milliseconds is asking every forked connection to wake up that often. A stage should return None when its options do not require ticking, which is what rate,interval=0 does.

A stage that needs a deadline rather than a cadence needs both halves: rearm so the count starts from the last byte, and a period some fraction of the deadline so that the host’s cadence rounds the answer by that fraction rather than by the whole thing. timeout asks for a quarter of its window and halts on the fourth consecutive idle tick, which is what bounds its error at a quarter rather than at one whole window.

Two details of the host’s timing are worth relying on. Reads win the race against the timer, so payload never waits behind bookkeeping. And a segment that fell behind does not then fire a burst of catch-up ticks: a missed schedule resumes from now, and a stage that wants to know how long it was away measures that itself.