-
Notifications
You must be signed in to change notification settings - Fork 1
US-188: require allkeys-lru on the blob-cache Redis and warn at boot #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Hazzng
merged 5 commits into
fix/187-poisoned-version-key-recovery
from
fix/188-redis-eviction-policy
Sep 19, 2026
+534
−3
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
07cadaa
US-188: require allkeys-lru on the blob-cache Redis and warn at boot
Hazzng 473d8c7
US-188: narrow the safe eviction policies and stop paging control-onl…
Hazzng 197c1e5
US-187: recover from a poisoned Redis version key instead of wedging …
Hazzng d57d327
Merge origin/main into fix/188-redis-eviction-policy
Hazzng 22686e5
Merge origin/fix/187-poisoned-version-key-recovery into fix/188-redis…
Hazzng File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| "sql-fs-api": patch | ||
| --- | ||
|
|
||
| Require `maxmemory-policy allkeys-lru` on the Redis backing the blob cache, and say so at boot instead of leaving it an unstated assumption. | ||
|
|
||
| **With Redis's default `noeviction`, a full Redis is a permanent outage, not a transient one.** An instance at `maxmemory` refuses every write with `OOM command not allowed`, and blob cache entries carry a 24 h TTL (`REDIS_BLOB_CACHE_TTL_MS`), so nothing ages out fast enough to make room. Someone has to flush keys or raise the limit by hand. The load harness measured that as 97.6% 5xx with no recovery, against a 6 s `CLIENT PAUSE` that recovered the moment the pause lifted. Nothing in the code or the docs asked for a different policy, so every deployment that took the Redis default was one memory spike away from it. With `allkeys-lru` the same pressure evicts cold blobs, which costs a Postgres read. | ||
|
Hazzng marked this conversation as resolved.
|
||
|
|
||
| **Only `allkeys-lru` and `allkeys-lfu` are accepted.** By default the data role shares an instance with the control role (`REDIS_DATA_URL` falls back to `REDIS_URL`), so whatever policy is set governs the exec-lock leases, version counters and destroy tombstones as well as the cache — which is what rules the other evicting policies out, each for its own reason: | ||
|
|
||
| - `allkeys-lru` / `allkeys-lfu` protect exactly the keys that must survive. A lease renewed every 20 s (`REDIS_EXEC_LOCK_RENEW_MS`) and a version counter touched on every write are the most recently and most frequently used keys in the instance, so a cold blob is always the better candidate. | ||
| - `allkeys-random` is refused. It samples uniformly, so a live lease is exactly as likely to be reaped as the cold blob beside it. | ||
| - `volatile-*` is refused, but not on recency grounds — the argument that "it can reap a live lease, because the control keys carry a TTL" applies just as well to `allkeys-*`, where those keys are candidates too. The real problem is that it evicts ONLY keys carrying a TTL: once the instance fills with keys that do not (the RW-lock reader ZSETs, anything a later change adds), there is no eviction candidate left and it behaves exactly like `noeviction` — writes refused, no recovery without a human. `allkeys-*` always has a candidate. | ||
|
|
||
| **Documented in `CLAUDE.md` and `README.md` next to `REDIS_URL` / `REDIS_DATA_URL`**, both of which now point at a new "Redis eviction policy" section carrying the `CONFIG SET` line, that reasoning, and the log events. | ||
|
|
||
| **The boot check is a warning, and stays a warning.** `startEvictionPolicyCheck` runs `CONFIG GET maxmemory-policy` against the data client after `listen`. A policy that is neither `allkeys-lru` nor `allkeys-lfu` logs `event:"redis_eviction_policy_unsafe"` at `severity:"critical"` with the policy it found, the command to fix it, and the reason that particular policy was refused — "not allkeys-*" is not a usable reason to give an operator running `allkeys-random`. Managed Redis providers routinely forbid `CONFIG GET` — ElastiCache renames the command, an ACL-restricted user answers `NOPERM` — so that case is recognised on its own and logged as `event:"redis_eviction_policy_unknown"`, `reason:"config_get_denied"`, with a line telling the operator to confirm the setting with their provider. A `CONFIG GET` that fails for any other reason, or answers something unparseable, logs the same event with `reason:"config_get_failed"`. Nothing here can fail startup: the check is never awaited (a slow Redis must not delay `listen`), it never rejects, and it is skipped when no Redis is configured. | ||
|
|
||
| **It is also skipped when the data client carries no data plane.** `REDIS_DATA_URL` falls back to `REDIS_URL`, so with `REDIS_BLOB_CACHE_ENABLED=false` and no path snapshot, the "data" client IS the control instance — and a correctly-configured control-only deployment would have been paged with a `severity:"critical"` line whose remediation makes things worse: switching a control-only Redis to `allkeys-*` makes its exec-lock leases, version counters and destroy tombstones evictable. The check now runs only when the blob cache or the path snapshot is actually enabled. | ||
|
|
||
| **Cost: one extra Redis round trip per boot, and a critical-severity line that most deployments will see on their first restart after upgrading.** That is the intended outcome, but it does mean an operator who alerts on `severity:"critical"` gets paged by an upgrade rather than by an incident, and the honest answer is to fix the policy rather than to filter the event. The check deliberately does not look at `maxmemory` itself: a Redis with no limit set never evicts and never refuses, so the policy is the only thing worth asserting, and an instance sized by its container rather than by `maxmemory` would produce a confusing second warning. It also says nothing about the control-plane Redis when the two are split, because `allkeys-*` is not obviously right there: LRU-evicting a version counter resets it to 1 under a replica that still holds a higher `lastSeenVersion`, which is the reset wrap the H6 TTL design closes. | ||
|
|
||
| Verified against the harness FAULT replica and its disposable Redis on :6380. `CONFIG SET maxmemory-policy noeviction` then restart produced the `redis_eviction_policy_unsafe` line in `fault.log`; `allkeys-lru` then restart produced `{"event":"redis_eviction_policy","policy":"allkeys-lru"}` and zero unsafe lines. The permission-denied path was exercised live too, with a Redis ACL user carrying `+@all -config`: the replica logged `reason:"config_get_denied"` with the `NOPERM` text and still came up `{"status":"ok"}` on `/healthz`. The ACL user was deleted and the original policy (`noeviction`) restored afterwards. `concurrency.mjs` stays all-PASS, exit 0. Twelve unit tests, each checked against a mutation of the code it covers rather than only against the module's absence: accepting every policy as safe, dropping the `OOM`-is-not-a-permission-error carve-out, never classifying a refusal as denied, dropping the RESP3 map reply branch, letting `CONFIG GET` errors escape, warning on a healthy boot, and logging synchronously each kill exactly the tests that claim to cover them. | ||
|
|
||
| **Not verified.** No test drives a real Redis to `maxmemory` under either policy, so the 97.6% figure is quoted from #167's harness run and not re-measured after the #185 role split; the recovery difference between the two policies is a property of eviction and the TTL rather than of this change. The server wiring itself has no unit test, because the bootstrap block only runs when the module is the process entry point, so the only proof that the check is actually called is the harness log, which is what was used. RESP3 map replies are covered by a fake, not by an ioredis client in RESP3 mode. And the check reads the policy once at boot: an operator who changes it afterwards gets no new warning until the next restart. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.