Repository navigation
fix: a cache that stopped being fed is noticed (ADR 0035) - #66
Merged
Merged
Conversation
`HasSynced` is a one-way latch: it turns true when the first LIST succeeds and never turns back. A cache whose watch is refused afterwards therefore kept reporting availability while its store answered from what it last held, so `workload_policy` and `cluster_policy` went on declaring every source read — indefinitely, and identically at every capture. ADR 0033 §5 recorded this as known and unfixed. Two things about that were wrong. For a superseding snapshot, stale is not the word: the payload states what is true at `captured_at`, and the omission of `unavailable_sources` is itself the claim that nothing was missed. And the latch is not a property of the seven policy caches but of every informer — a refusal on pods or the owner chain is not a degraded payload but a blind agent producing payloads of ordinary shape. Watch failures are now recorded per cache through SetWatchErrorHandler, the only signal client-go offers. The cause is not examined, as ADR 0033 §2 already decided; persistence does the work classification would. A run of failures ends after two minutes of quiet, which is the reflector's own `defaultBackoffReset` and its own reason. A gating cache failing for five continuous minutes stops the agent, which is what the comment above PodWatcher.Run always said should happen and the agent could not do. The watchdog runs from before the first sync, so the never-synced case ends the same way — that hole was worse than the one this started from: `WaitForCacheSync` has no timeout, so an agent installed without the `pods` grant sat there forever holding nothing and reporting nothing. A policy cache that failed within the last three minutes declares itself unavailable through the field that already exists. The informers now run on the watchdog's context rather than the caller's: `factory.Shutdown()` waits for every informer it started, so a verdict reached in five minutes was held until the process was signalled. Verified on kind, both directions. Denied the pods grant: the agent exits naming the resource and the pod restarts (332s). Revoked a policy grant from a running agent: the payload declares the source while metadata collection continues and the controller never restarts (366s). With the watchdog and the health check disabled — the previous behaviour — the first fails after 11 minutes still running, and the second times out reporting `last declared sources = []`. No payload shape, no new kind, no new RBAC.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes the limitation ADR 0033 §5 recorded as known and unfixed.
The hole
HasSyncedis a one-way latch. It turns true when the first LIST succeeds and never turns back, so a cache whose watch is refused afterwards keeps reporting availability while its store answers from what it last held. The reflector retries forever in the background.ADR 0033 §5 judged the damage to be staleness. For a superseding snapshot that is the wrong word:
workload_policyandcluster_policystate what is true atcaptured_at, and the omission ofunavailable_sourcesis itself the claim that nothing was missed. A budget deleted after the refusal keeps shipping; one created after it never ships. The report is not behind — it is wrong, identically at every capture, until someone restarts the agent. And §5's mitigation ("any restart reports correctly") rests on an event this project deliberately made rare: a single-replica Deployment with no volume runs for weeks.The latch is also not a property of the seven policy caches. It belongs to every informer — and a refusal on
podsor the owner chain is not a degraded payload but a blind agent producing payloads of entirely ordinary shape.There was a third case, worse than the one this started from.
WaitForCacheSynchas no timeout, so an agent installed without thepodsgrant never synced, never errored and never exited: a Running pod, a healthy Deployment, no data, and nothing anywhere saying why.What changed
Watch failures are recorded per cache through
SetWatchErrorHandler— the only signal client-go offers, since there is no callback for success.defaultBackoffReset, for its own reason: while failing it retries at most every [30,60) seconds. A streak is measured between its first and last failure, never up to now, so one dropped connection is never an outage.PodWatcher.Runalways claimed happened and the agent could not do.A real bug found on the way
The watchdog reached its verdict on time and
Runreturned 30 seconds later.factory.Shutdown()waits for every informer it started, and they were started on the caller's context — so they would stop only when the process was signalled. The informers now run on the watchdog's context. In a live cluster that is the difference between exiting in six minutes and hanging until SIGTERM.Verified on kind, both directions
podsgrantWith the watchdog and the health check disabled — which is the previous behaviour — the first fails after 11 minutes still running (
waiting on a sync that will never come) and the second times out printing the old lie verbatim:last declared sources = [], thirteen minutes after the grant was taken away.Both e2e tests are slow by nature, and the reason is the mechanism: an established watch is not re-authorized, so a revocation lands only when client-go re-opens the connection, every 5–10 minutes. ADR 0035 §7 records that lag as a decision, along with why polling permissions via SelfSubjectAccessReview was rejected — a write verb in the audit log of a product whose promise is that it only reads.
No payload shape, no new kind, no new RBAC.
🤖 Generated with Claude Code