docs(code): honest rationales — Akka restart semantics, per-key shards, EF literal inlining; retire or annotate orphaned members
This commit is contained in:
+18
-10
@@ -446,27 +446,35 @@ for the life of a generation, so memoizing the walk is sound. A key's `ExpiresUt
|
||||
*clock*, not with a deployment — so revocation and expiry are re-checked on every call, outside the
|
||||
cache. Caching them would have let an expired key keep working until the next deploy.
|
||||
|
||||
**The cache is sharded per session, because a flat one let any client evict every other client's
|
||||
decisions.** It used to be a single dictionary with the session identity folded *into* the composite key
|
||||
**The cache is sharded per calling KEY, because a flat one let any client evict every other client's
|
||||
decisions.** It used to be a single dictionary with the caller identity folded *into* the composite key
|
||||
and one process-wide bound of `MaxEntries = 100_000`, whose only eviction was a wholesale `Clear()` at
|
||||
the cap. So one machine consumer crawling a large address space past 100k entries wiped the memoized
|
||||
walks of every other session on the server, and each store paid a `ConcurrentDictionary.Count` — an
|
||||
all-locks snapshot — to test the cap. It is now a dictionary of per-session shards keyed by the session's
|
||||
`ApiKeyId ?? Principal`, each holding its own `(operation, realm, nodeIdentifier)` entries, with two
|
||||
walks of every other caller on the server, and each store paid a `ConcurrentDictionary.Count` — an
|
||||
all-locks snapshot — to test the cap. It is now a dictionary of shards keyed by
|
||||
**`ApiKeyId ?? Principal`**, each holding its own `(operation, realm, nodeIdentifier)` entries, with two
|
||||
bounds:
|
||||
|
||||
| Bound | Value | What it clears |
|
||||
|---|---|---|
|
||||
| `MaxEntriesPerSession` | 20 000 | that one session's shard |
|
||||
| `MaxEntriesPerSession` | 20 000 | that one **key's** shard |
|
||||
| `MaxEntries` | 100 000 | every shard (the shard *objects* are dropped, not emptied) |
|
||||
|
||||
Both are compile-time constants, not appsettings keys. Five sessions simultaneously at their per-session
|
||||
ceiling reach the global cap; **the number of shards is not itself bounded** — the bound is entries.
|
||||
⚠️ **The shard key is the API key, not the session — the constant's name predates it.** Every session
|
||||
authenticating with the same `otua_<keyId>_…` credential shares one shard and one 20 000-entry budget,
|
||||
so a crawl by one of them *can* clear the memoized decisions of its sibling sessions. What the sharding
|
||||
guarantees is isolation **across distinct keys**: one key's crawl can never evict another key's entries.
|
||||
(An Anonymous session shards under the reserved `anonymous` principal, so all of them likewise share
|
||||
one shard.)
|
||||
|
||||
Both bounds are compile-time constants, not appsettings keys. Five keys simultaneously at their
|
||||
per-key ceiling reach the global cap; **the number of shards is not itself bounded** — the bound is
|
||||
entries.
|
||||
|
||||
Things to know before changing it:
|
||||
|
||||
- **There is no session-close eviction hook**, deliberately. A closed session's shard is shed the next
|
||||
time a wholesale clear happens — a generation bump, or the global cap tripping. Dropping the shard
|
||||
- **There is no session-close eviction hook**, deliberately. The shard of a key with no live sessions
|
||||
left is shed the next time a wholesale clear happens — a generation bump, or the global cap tripping. Dropping the shard
|
||||
objects rather than emptying them is what makes that sweep work.
|
||||
- **The counters are approximate under concurrency, by design.** The cap test is a check-then-act over
|
||||
`Volatile.Read` of `Interlocked` counters rather than a lock, so concurrent stores can overshoot a cap
|
||||
|
||||
Reference in New Issue
Block a user