Repository navigation
fix: both halves of the counter reading come from one observation (ADR 0043) - #75
Merged
Merged
Conversation
…R 0043) restart_counters reports two numbers a consumer is told to subtract: the kubelet's counter, and what it already stood at when this agent process first saw the container. They came from different places. `restarts` was read from the informer's shared store through the pod lister; `restarts_before_observation` from the restartCounts map the agent maintains in its own event handler. client-go updates the shared store *before* it dispatches to that handler, so for the span of one dispatch the store holds a counter the baseline has not seen. Usually invisible, because the counter only goes up. Visible when it goes down — a StatefulSet replacing a pod under the same name: the store already says 2 while the baseline still says 40. The payload then asserts that forty restarts predate an observation of a counter standing at two, and the subtraction docs/backend-requirements.md prescribes yields -38. It had been showing up as a test that failed about one run in three under package load, which is the shape a race takes when nobody has looked at it. Both values now come from one read of the baseline: `restarts` is base.last, the counter as the agent last observed it, and `restarts_before_observation` is base.atFirstSight from the same struct under the same lock. The ordering becomes structural rather than probable — a rebaseline sets both together, an advance only raises last — so the difference cannot be negative whatever the informer is doing. The lag this introduces is one informer dispatch against a payload that flushes once a minute, and it makes the field consistent with everything else in this payload, which is already a statement about what this agent process observed. The regression test builds the interleaving directly — baseline at 40, store at 2, handler not yet run — rather than waiting for it. Checked in both directions: with base.last it passes; with the lister's value restored it fails with "reading claims 40 restarts predate observation of a counter at 2; ADR 0034 tells a consumer to subtract these and they would get -38". Evidence: five consecutive `go test -race -count=1 ./internal/collector` runs green, where the old test failed roughly one in three. restarts-e2e PASS 148.6s, both suites, with a real reading of Restarts:3 RestartsBeforeObservation:2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
restart_countersreports two numbers a consumer is explicitly told to subtract: the kubelet's counter, and what it already stood at when this agent process first saw the container. They came from different places.restartswas read from the informer's shared store through the pod lister.restarts_before_observationcame from therestartCountsmap the agent maintains in its own event handler. client-go updates the shared store before it dispatches to that handler, so for the span of one dispatch the store holds a counter the baseline has not seen.Usually invisible — the counter only goes up, and the reading is stale in a way nobody could detect. It becomes visible when the counter goes down, which is a StatefulSet replacing a pod under the same name: the store already says 2 while the baseline still says 40.
The payload then asserts that forty restarts predate an observation of a counter standing at two.
docs/backend-requirements.mdtells a backend to subtract those:This had been surfacing as a test that failed about one run in three under package load — the shape a race takes when nobody has looked at it yet.
The fix
Both values now come from one read of the baseline:
restartsisbase.last— the counter as the agent last observed it — andrestarts_before_observationisbase.atFirstSightfrom the same struct, under the same lock.That makes the ordering structural rather than probable. A rebaseline sets
lastandatFirstSighttogether; an advance only ever raiseslast. Sorestarts >= restarts_before_observationholds by construction, whatever the informer is doing at the time.The lag introduced is one informer dispatch against a payload that flushes once a minute — and it makes this field consistent with everything else in the payload, all of which is already a statement about what this agent process observed. A counter read from somewhere the agent had not yet processed was the odd one out.
The test is the interleaving, not a timing hope
The regression test builds the state directly — baseline at 40, store at 2, handler not yet run — by injecting a stub lister, because that is exactly what the informer produces for a moment on every counter change. Waiting for it to happen by chance is what let this sit unnoticed.
Checked in both directions. With the lister's value restored:
Evidence
go test -race -count=1 ./internal/collectorruns green, where the previously flaky test failed roughly one in three.make restarts-e2ePASS 148.6s, both suites, against a real cluster — including a real day-one reading:Restarts:3 RestartsBeforeObservation:2, difference positive.go test -race ./...,make lint(0 issues),make chart-lint,go vet -tags e2e ./...clean.Docs
docs/adr/0043-…amends 0034 §7, which describes the reading as assembled from the live pod index at an instant. The rest of the record still is — pod creation, container start, last termination. The counter is not, and the ADR says so rather than hiding the lag.docs/backend-requirements.mdnow states the invariant a backend can rely on:restarts - restarts_before_observationis never negative. It also says it was not always so, so anyone carrying a defensive clamp knows why.docs/security.md's payload table follows.Named, not solved
last_terminationstill comes from the pod, so it can describe a death the counter has not yet counted. That direction is benign — it under-reports rather than inventing history — and self-corrects on the next flush. Recorded in the ADR so it is a known shape rather than a surprise.🤖 Generated with Claude Code