The owner short-circuit in PgAclEngine::check ran a PK query (SELECT user_id FROM storage.folders/files WHERE id=$1) on every authorization check of a folder/file — the common case, since users mostly act on their own resources. Memoise it in an owner_cache (moka, TTL 300s, 100k cap). The owner column is immutable, so this is safe: the cache maps resource -> real owner and can never grant a non-owner access (a different caller's owner==uid test fails against the cached owner and falls through to grants); a hard-deleted resource that briefly resolves to its former owner simply fails later at execution with NotFound. The per-check sql_queries counter now increments only on a miss. Removes 1 DB query + 1 pool-connection acquisition per owner check. Magnitude is deployment-specific (query latency x whether the pool is contended); see benches/ACL-OWNER-CACHE.md. Also adds two DB perf-investigation harnesses, gated behind the `bench` feature (need the dev Postgres; zero prod impact): - examples/bench_db_pool.rs + benches/DB-POOL.md — pool size vs tail latency - examples/bench_owner_cache.rs + benches/ACL-OWNER-CACHE.md — owner query vs cache Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.4 KiB
ACL owner-cache benchmark
Opportunity #2 from the backend perf investigation. PgAclEngine::check runs an
owner short-circuit on every authorization check of a folder/file — the common
case (a user touching their own resources). That was an uncached PK query
(SELECT user_id FROM storage.folders/files WHERE id=$1) per check. Now
memoised in an owner_cache (moka, pg_acl_engine.rs); the owner column is
immutable so it's safe to cache.
Safety
The cache maps resource → real owner. It can never grant a non-owner
access: a different caller's owner == uid test fails against the cached real
owner and falls through to the grant lookup. The only staleness is a
hard-deleted resource briefly resolving to its former owner — harmless, since the
operation then fails at execution with NotFound, and no new access is granted.
TTL 300 s, capacity 100 k. (Owners never change, so no invalidation hook needed.)
Reproduce
docker compose up -d postgres # needs ≥1 folder in the dev DB (just load-seed)
cargo run --release --features bench --example bench_owner_cache
# tunables: BENCH_CONCURRENCY (64), BENCH_POOL_SIZE (20 = prod default), BENCH_SECONDS (4)
Models the owner short-circuit: the exact owner query vs a moka hit, under concurrency, against the real dev Postgres.
Results (14 cores, local Docker Postgres)
Burst — C = 64, pool = 20 (pool-saturating):
| mode | DB queries | checks/s | p50 µs | p95 µs | p99 µs | max µs |
|---|---|---|---|---|---|---|
| uncached (before) | 32746 | 8172 | 7063.0 | 11323.9 | 20135.1 | 106015.7 |
| cached (after) | 1 | 6114546 | 1.46 | 3.00 | 3.79 | 75111* |
Low load — C = 8, pool = 20 (no queueing, isolates pure query cost):
| mode | DB queries | checks/s | p50 µs | p95 µs | p99 µs | max µs |
|---|---|---|---|---|---|---|
| uncached (before) | 25432 | 6357 | 1097.5 | 2091.5 | 2433.9 | 10105.8 |
| cached (after) | 1 | 5076363 | 0.88 | 5.00 | 10.92 | 2342* |
* max for the cached path is a tokio-scheduler / allocation outlier, not the
cache — p99 (µs) is the meaningful tail.
Conclusions
-
Per owner check, 1 DB query → 1 memory hit. Pure query cost (C=8): the cache saves ~1.1 ms p50 / 2.4 ms p99 per check (the owner query latency on this Docker-on-macOS Postgres).
-
Under burst it compounds with the pool (opportunity #1). At C=64/pool=20, the uncached checks both pay the query and queue on
acquire()→ p50 7 ms, p99 20 ms. The cache removes the query entirely → no pool occupancy → p99 3.8 µs. Fewer queries ⇒ less pool pressure ⇒ lower tail latency under exactly the conditions that cause the cliff. -
Caveat: the absolute ms here are inflated by the local Docker Postgres query latency (~1–2.4 ms; a tuned local PG would be faster, a networked/loaded one similar or worse) and the deliberately induced pool pressure. The robust, deployment-independent facts are: 1 fewer DB query and 1 fewer pool acquisition per authorized action on an owned resource — which is most actions, since users mostly touch their own files.
-
Scope: this is the owner short-circuit (the hot path). Non-owner checks still do the cascade grant query (unchanged); group expansion was already cached.