Skip to main content

Module warming_allowance

Module warming_allowance 

Source
Expand description

Per-source serve-vindicated warming allowance (ADR 041).

Speculative “warming” buys pull a blob before any client asks for it. The buy can be wrong: a dud that never gets a second serve. This module bounds the resulting loss per upstream source node (a bonded seller identity), so a flood of at-market one-hit blobs from one attacker source can only drain that source’s small allowance, never the operational deposit.

Each source holds an allowance that starts full at budget and is clamped to [-budget, +budget]. A never-touched source has no ledger entry and reads as full. A speculative buy debits the source’s ledger by the buy cost and tags the blob’s hash with its source. Every serve of a tagged hash credits the realized margin back to that source’s ledger. A blob re-served often enough refunds its buy (vindicated; at a market price and the default operator share, two serves do); a dud served once keeps the fee skim as a loss. The source keeps warming while its allowance is positive, so its duds spend the budget down, and once they exhaust it the source is blocked from further speculative buys until it recovers. Eviction forgets the tag so a stale hash can never credit a ledger again. A slow time-refill on top of the serve credits forgives a transient bad patch.

§Seam

WarmingAllowance is the ledger; WarmingCreditSink is the serve-path boundary in front of it, mirroring crate::receipt_log::ReceiptSink. A stream task ends its serve with a non-blocking WarmingCreditSink::credit, and the single background task from spawn_warming_creditor applies the credit to the ledger. Tests use the inline DirectWarmingCreditSink (a test-support type) to keep the credit observable synchronously.

The bucket ledger is a plain Mutex<HashMap> and every operation on it is O(1) — nothing iterates under the lock. The seam is not there to escape a long hold. It is there so a serve’s final step joins no queue at all: a stream that has finished its bytes still holds its lane slot and its floor reservation until that step returns, so waiting on a lock somebody else happens to hold costs a serve slot for the length of the wait.

Two things take that lock in the shipped wiring. The buy loop takes it on the cache-miss path — WarmingAllowance::available to gate a speculative pull, then WarmingAllowance::debit_speculative when one goes ahead. The aggregator takes it to apply each queued credit. Eviction does not: its only contact with the allowance is WarmingAllowance::forget, which touches the tag map alone.

The hash-to-source tags live outside that lock in a DashMap, so the serve path resolves the source itself and enqueues (source, units). That is what keeps a deferred credit honest: the source is bound at serve time, so an eviction and a re-warm from a different source between enqueue and apply cannot redirect the credit (see WarmingCreditSink).

Structs§

NoopWarmingCreditSink
A WarmingCreditSink that drops every credit. It is the inert default a fresh crate::handlers::client::ClientHandlerDeps carries before the runtime overwrites it with the channel sink from spawn_warming_creditor. A handler left with this default applies no warming credit, which only keeps each source’s ledger more conservative — the same safe direction a full or closed aggregator queue takes.
SourceId
The bonded upstream seller node a speculative warming buy is charged to.
WarmingAllowance
Bounds speculative warming losses per upstream source node.

Constants§

WARMING_CREDIT_CAPACITY
Capacity of the warming-credit queue. One credit is produced per completed serve, and the aggregator applies each with a couple of in-memory map operations, so this absorbs a burst far larger than the drain can fall behind on.

Traits§

WarmingCreditSink
Non-blocking enqueue boundary for warming serve credits on the serve path.

Functions§

spawn_warming_creditor
Spawn the single background task that owns the credit queue and applies each credit to allowance, returning the WarmingCreditSink the serve path enqueues through and the StopHandle that stops the task.