Files
Oxicloud/benches/ACL-OWNER-CACHE.md
T
DioCrafts 778d551090 perf(authz): cache resource owner lookups in PgAclEngine
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>
2026-06-21 16:56:11 +02:00

70 lines
3.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
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
1. **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).
2. **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.
3. **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.
4. 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.