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§
- Noop
Warming Credit Sink - A
WarmingCreditSinkthat drops every credit. It is the inert default a freshcrate::handlers::client::ClientHandlerDepscarries before the runtime overwrites it with the channel sink fromspawn_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. - Source
Id - The bonded upstream seller node a speculative warming buy is charged to.
- Warming
Allowance - 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§
- Warming
Credit Sink - 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 theWarmingCreditSinkthe serve path enqueues through and theStopHandlethat stops the task.