Documentation
¶
Overview ¶
Package limit is one rate limit for every replica.
A counter in a process's memory is three limits when three pods are running, and none after a deploy: an attacker gets the limit multiplied by the replica count and gets it back whenever anything restarts. That is why the auth module's lockout said so in its own comment and left the real thing for later. This is the real thing — one row per key per window in Postgres, one statement to record an event, one to read a count — so the answer does not depend on which pod the request landed on.
It is a fixed window rather than a token bucket: a limit stated the way a person understands it ("ten in a quarter of an hour"), one INSERT ... ON CONFLICT to record it, and the worst the edge between two windows gives an attacker is twice the limit for one instant.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNoConnection = errors.New("limit: no database connection on this context")
ErrNoConnection is what every method answers when Connections finds none. It is an error rather than a silent allowance because the caller decides: auth fails open and logs, which is right for a lockout and would be wrong for a paywall.
Functions ¶
Types ¶
type Connections ¶
Connections is where a limiter finds the pool, and httpx.ConnFrom is what the application passes: the kernel puts the connection on every request's context.
It is a parameter rather than an import for two reasons. A composition builds its modules before kit/app opens the pool, so a limiter cannot be handed one at construction; and a package that counts rows has no business linking a web server to find out where they go — modules/auth's contracts package holds a Limiter, and a contracts package is the entity and the interfaces.
type Limiter ¶
type Limiter interface {
// Allow records one event under key and reports whether the window is
// still within limit: the limit-th event is allowed and the one after it is
// not. retryAfter is what is left of the window, and it is zero when the
// answer is yes.
Allow(ctx context.Context, key string, limit int, window time.Duration) (ok bool, retryAfter time.Duration, err error)
// Count reports how many events key has in the window that is open now, and
// how long that window has left. It records nothing, because a limit with
// three answers rather than two — allow, delay, refuse — has to be read
// before the attempt it is about, and a read that counted would make every
// successful sign-in an attempt against the lockout.
Count(ctx context.Context, key string, window time.Duration) (n int, retryAfter time.Duration, err error)
// Forget drops a key: the caller has been proved right about whoever they
// were counting.
Forget(ctx context.Context, key string) error
}
Limiter counts events under a key and answers whether one more is within a limit.
A key is whatever the caller counts by — an address, an account, the two together — and it is scoped to the tenant of the context before it is stored, so two customers never share a counter and no caller has to remember to say which tenant it is counting in.
func Postgres ¶
func Postgres(conns Connections) Limiter
Postgres returns the limiter every replica shares.