Integrity, resilience and errors¶
What signs and encrypts entries, RFC 8785 canonical JSON, the backends' circuit breaker, and
the exceptions.
In fastapi_alcazaba.core.
In the guide: Security model, Encrypted entries, Resilience.
fastapi_alcazaba.core.Signer
¶
Signer(keys: Secrets)
Signs what the cache stores and only accepts what it signed itself.
Whoever can write to the backend (a shared Redis, a leaked password) can't plant data, nor a pickle that runs code when it's read, nor copy one user's entry under another user's key: the tag covers the key too. The first key signs; the others are only checked, so a key can be rotated without losing the cache: put the new one first and drop the old one later.
unsign
¶
unsign(cache_key: str, data: bytes) -> bytes | None
The payload, or None when data wasn't signed by us for this key.
fastapi_alcazaba.core.canonical_json
¶
canonical_json(value: Any) -> bytes
value (dicts with string keys, lists, tuples, strings, integers, floats, booleans and
None) in its canonical form, as UTF-8. Raises ValueError for what I-JSON leaves out (integers
a double can't hold exactly, NaN, infinities, lone surrogates) and TypeError for what isn't
JSON.
fastapi_alcazaba.core.Encrypter
¶
Encrypter(keys: Secrets)
Encrypts what the cache stores and only accepts what it encrypted itself.
On top of what signing gives (whoever can write to the backend can't plant data, run a pickle or move one user's entry under another user's key), a dump or a backup of Redis doesn't reveal the cached responses. The cache key is the associated data, so an entry only decrypts under its own key. Each entry is encrypted with its own AES-256 key, derived with HKDF-SHA256 from the secret and a random salt, so random 96-bit nonces never get near the limits of AES-GCM however many entries are written.
The first secret encrypts; all of them decrypt, so a secret can be rotated without losing the cache: put the new one first and drop the old one later.
Layout: b"fastapi_alcazaba/e1" | key id (4) | salt (16) | nonce (12) | ciphertext and GCM tag (16).
open
¶
open(cache_key: str, data: bytes) -> bytes | None
The payload, or None when data wasn't encrypted by us for cache_key.
fastapi_alcazaba.core.CircuitBreaker
¶
CircuitBreaker(failure_threshold: int = 5, reset_timeout: float = 30.0)
Stops calling a failing backend for a while.
Closed: calls go through, and failure_threshold consecutive failures open it. Open: calls are
refused for reset_timeout seconds. Then a single trial call goes through (half open): a
success closes it, a failure opens it again. One breaker per process.
retry_after
property
¶
retry_after: float
Seconds until a trial call may go through; 0 unless it's open.
allow
¶
allow() -> str | None
The state the call goes through in: "closed", or "half-open" when it's the single trial; None when it's refused.
abandon_trial
¶
abandon_trial() -> None
The trial call ended without a verdict (cancelled): let the next call try. Only for the
call allow() let through "half-open": another one would let a second trial through.
fastapi_alcazaba.core.errors
¶
Warnings and exceptions of Alcazaba.
UnsignedEntriesWarning
¶
Bases: UserWarning
A coder whose decoding can run code (PickleCoder) is used without a signing key.
UnstableCacheKeyWarning
¶
Bases: UserWarning
An argument in the cache key has a repr() that changes with every call (it shows a memory
address), so the entry is never found again: leave it out with exclude_params.
BackendUnavailable
¶
Bases: Exception
The backend failed, timed out or its circuit breaker is open. The decorator treats it as "no cache" and runs the function.
CircuitOpen
¶
Bases: BackendUnavailable
The circuit breaker is open: the backend isn't called for a while.