Skip to content

feat: a confirmed endpoint is named on the port it answered on (ADR 0082) - #123

Merged
kfeofantov merged 2 commits into
mainfrom
feat/pprof-endpoint-identity
Sep 16, 2026
Merged

kfeofantov merged 2 commits into
mainfrom
feat/pprof-endpoint-identity

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

Two slices. The CI one-liner rides along because it is one line, and because the payload change below is exactly the kind of PR a false red costs the most on.


1. listening_ports names the port that served pprof (ADR 0082)

Three facts about a workload's debug surface already shipped; the fourth did not.

Fact Payload Identity
the binary links net/http/pprof go_build.pprof_endpoint image digest
the process listens here, loopback or not listening_ports workload, container, digest
six confirmed, eleven absent, two unreachable collection_coverage.pprof counts only
which port of which workload answered — nowhere

The gap could not be closed by joining what shipped. In the golden payload shop/web links pprof and binds 8080 and loopback 6060 — one of those serves /debug/pprof/heap, the contents of the process's memory, to anything that can reach the pod, and the other serves it to nothing. An inference from "links pprof" and "has a routable port" is right most of the time and wrong silently.

The shape

The prober's key is Target{ImageDigest, Port} and a listening_ports record carries both, so the join is a lookup and needs no new payload, no new kind, and no new identity — the workload is already named on that record. Prober.Stamp writes it: the package that owns the answer writes it down, and the store stays a pure join of what nodes reported.

Empty is "not asked", never "not pprof." Candidates drops loopback ports before the funnel, so no-answer is the common case. A boolean would have collapsed the two claims this payload has to keep apart.

inventory.Port is new and carries the node's observation plus the controller's answer. nodescan.ListeningPort goes on carrying only what a node can know, which is what crosses the channel. One field on the node's type that the node never fills would have been shorter and would have blurred that line.

What the ADR argues, beyond the mechanism

  • Why this names a workload where the coverage payload does not. Invariant 6 protects the customer's refusal — they said do not look, and we do not report on whom. This is the opposite: an admitted workload, under collection, and a fact about its own configuration. A workload the profiling opt-out excludes is never probed, so it has no answer to stamp.
  • The objection, named. The payload becomes a list of debug endpoints inside the cluster. Weak — the ports are already in this payload, the mux answers any pod whether or not we write it down — but weak is not absent.

Verification

make inventory-e2e — PASS (726s)
  endpoint discovery confirmed 1 target(s)
  listening_ports names the endpoint: map[6060:confirmed 9090:]

The sample binds both: 6060 routable and serving the index, 9090 on loopback and never asked. One says so and the other stays silent.

Unit tests cover the stamp, including that an answer does not cross builds — the digest is half the key. The golden payload shows all three states plus the empty one.


2. The linter runs without its analysis cache

On 2026-09-16 a restored cache reported six SA5011 findings in files the branch did not touch, twice, on a tree byte-identical to a passing main — the changed files were behind //go:build e2e, which .golangci.yml sets no build tag for, so the linter never read them. Deleting the cache entry and re-running gave 0 issues.

The findings were false: each site calls t.Fatal in the nil branch before dereferencing, and t.Fatal calls runtime.Goexit(). Seeing that takes knowing testing.common.Fatalf does not return, which a partial analysis cache drops.

Measured: warm 15.6s, cold 39.2s. 24 seconds a run for the whole class of false red.


go test ./...                    pass
golangci-lint (pinned v2.11.4)   0 issues
git archive HEAD -> clean tree   builds and tests green

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA

kfeofantov and others added 2 commits September 16, 2026 14:23
A restored cache reported six SA5011 findings in files the branch did not
touch, twice, on a tree byte-identical to a passing main — the changed files
were behind `//go:build e2e`, which `.golangci.yml` sets no build tag for, so
the linter never read them. Deleting the cache entry and re-running gave 0
issues.

The findings were false: each site calls t.Fatal in the nil branch before
dereferencing, and t.Fatal calls runtime.Goexit. Seeing that takes knowing
testing.common.Fatalf does not return, which a partial analysis cache drops.

    warm 15.6s, cold 39.2s — 24 seconds a run

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
…082)

Three facts about a workload's debug surface already shipped. The binary
links net/http/pprof, per digest. The process listens on these ports, some
loopback, per workload. Six targets confirmed, per nobody — the prober's
answer left the cluster as four integers.

So the payloads could not say which port served the mux. In the golden,
shop/web links pprof and binds 8080 and loopback 6060; one of those serves
/debug/pprof/heap to anything that can reach the pod and the other serves it
to nothing, and the difference was unrecoverable. An inference from "links
pprof" and "has a routable port" is right most of the time and wrong
silently, which is the worst thing a report can be.

The answer is stamped onto the port, in the payload that already names the
workload: the prober's key is {digest, port} and a record carries both, so
the join is a lookup. Prober.Stamp does it — the package that owns an answer
writes it down — and the store stays a pure join of what nodes reported.

Empty is "not asked", never "not pprof". Candidates drops loopback ports
before the funnel, so no-answer is the common case; a boolean would have
collapsed the two claims this payload needs to keep apart.

inventory.Port is new and carries the node's observation plus the
controller's answer. nodescan.ListeningPort keeps carrying only what a node
can know, which is what crosses the channel — one field on the node's type
that the node never fills would be shorter and would blur that line.

No new request, no new read, no new connection: the probe ADR 0057
authorised already established this, and what changes is where it is written.

    go test ./...                    pass
    golangci-lint (pinned v2.11.4)   0 issues
    make inventory-e2e               PASS (726s)
      listening_ports names the endpoint: map[6060:confirmed 9090:]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
@kfeofantov
kfeofantov merged commit 72448c4 into main Sep 16, 2026
4 checks passed
@kfeofantov
kfeofantov deleted the feat/pprof-endpoint-identity branch September 16, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant