Module quota

Module quota 

Source
Expand description

F3’s per-actor write quota: how much one actor may turn into permanent storage.

docs/14 F3 splits the “events are forever” problem in two. Channel (a) — rejected garbage — was closed by §3.7’s rule that only validated events are durably logged, so a refused command leaves nothing behind. Channel (b) is the one this module is for:

validated spam from a legitimate but abusive session — permanent by design. Remediation: per-actor rate/volume quotas … on by default with generous limits.

On by default is the load-bearing half. A quota a program has to ask for is a quota most programs do not have, and F3’s whole point is that the default deployment should not turn an abusive session into a permanent cost.

§The table is bounded, which is the part that is easy to get wrong

The obvious implementation is a map from actor to a counter. That map is unbounded memory keyed by a string the client chooses — the same denial of service the quota exists to prevent, moved one level down and made harder to see. Under DevIdentity the actor is whatever the client says, so an attacker sending a fresh name per proposal would both evade the quota and grow the table.

So the counters are sharded: a fixed number of buckets, an actor hashed into one, and no per-actor allocation ever. Memory is BUCKETS × a few bytes, for the life of the process, whatever arrives. Two consequences, both deliberate:

  • Two actors can share a bucket, and therefore a budget. That is why the limit is generous rather than tight: a shared bucket must still be ample for both. Quota::default says what the numbers are and why.
  • The hash is keyed per process (RandomState), so a client cannot compute a name that lands in a chosen bucket. Without that, sharing a bucket stops being an accident an operator accepts and becomes a way to spend somebody else’s budget on purpose.

§What this is not

It binds an actor, so it is worth exactly what the actor is worth. Under crate::identity::DevIdentity the actor is the claim the client sent, so an attacker who rotates names spreads across buckets rather than being stopped — the total is still bounded by BUCKETS × the limit, which is a bound rather than the bound anybody wanted. docs/48 is the seam that fixes this, and docs/82 §82.5 is the composition written out rather than left to be inferred.

Structs§

Quota
The limits, and the window they are counted over.
RateLimit
The sharded counters, and the process-random hash that decides which bucket an actor lands in.

Constants§

BUCKETS
How many counters exist, for all actors, forever.