Skip to content

Collect health-check and UDP retry evidence#36

Closed
MaurUppi wants to merge 5 commits into
mainfrom
fix/healthcheck-retry-evidence-1026-1029
Closed

Collect health-check and UDP retry evidence#36
MaurUppi wants to merge 5 commits into
mainfrom
fix/healthcheck-retry-evidence-1026-1029

Conversation

@MaurUppi

@MaurUppi MaurUppi commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • add atomic health-check diagnostics and Prometheus gauges to distinguish H1/H2/H3 lifecycle states
  • enrich UDP retry-limit warnings with attempted dialers, last write error, and probe age
  • add read-only Python evidence collectors for health-check metrics and UDP retry logs
  • address audit P2 by compressing only consecutive duplicate attempted dialers while preserving non-consecutive retry churn evidence

Review notes

  • UnregisterAliveDialerSet was intentionally reshaped to keep the refcount mirror symmetric without creating transient negative map entries; the observable registration lifecycle remains equivalent for valid register/unregister pairs.

Validation

  • python3 -m unittest scripts/test_issue_1026_evidence_scripts.py
  • GOWORK=off GOCACHE=/tmp/dae-healthcheck-gocache go test control/addr.go control/udp_retry_diagnostics.go control/udp_retry_diagnostics_test.go -count=1
  • GOWORK=off GOOS=linux GOARCH=amd64 GOCACHE=/tmp/dae-healthcheck-gocache go test -exec /usr/bin/true ./component/outbound/dialer -count=1

Not tested

  • full ./control and ./pkg/metrics runtime tests locally; this checkout hits existing eBPF generated/stub build-tag conflicts and Linux-only runtime constraints

Issue daeuniverse#1026 needs data that distinguishes exited health-check goroutines from blocked probe loops, and daeuniverse#1029 needs retry-limit warnings that carry enough context to confirm or reject selection churn. This adds atomic health-check diagnostics, Prometheus gauges, warning-level UDP retry evidence, and read-only Python collectors without changing the selected repair path for B-1/B-3.

Constraint: Full eBPF/control-plane tests require a Linux runtime and currently hit generated/stub build-tag conflicts locally.

Rejected: Implement self-healing or retry selection behavior in this batch | the design requires evidence collection before choosing those behavior changes.

Confidence: medium

Scope-risk: moderate

Directive: Do not treat these metrics as a root-cause fix; use collected H1/H2/H3 evidence before changing health-check lifecycle behavior.

Tested: python3 -m unittest scripts/test_issue_1026_evidence_scripts.py

Tested: GOWORK=off GOCACHE=/tmp/dae-healthcheck-gocache go test control/addr.go control/udp_retry_diagnostics.go control/udp_retry_diagnostics_test.go -count=1

Tested: GOWORK=off GOOS=linux GOARCH=amd64 GOCACHE=/tmp/dae-healthcheck-gocache go test -exec /usr/bin/true ./component/outbound/dialer -count=1

Not-tested: Full ./control and ./pkg/metrics runtime tests; local environment reports bpf_stub.go and bpf_bpfel.go redeclarations plus Linux-only eBPF/runtime constraints.
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

DNS Benchmark Compare

  • Base ref: origin/main
  • Head ref: f320411f0e63f5fae7a44c2459a09e3880a9f41e
  • Base strategy: merge-base
  • Suite profile: dns-module
  • Benchmark count: 3
  • Benchmark time: 200ms

Suite Status

  • control_dns_cache: failed
  • component_upstream_hotpath: success

control_dns_cache

DNS Benchmark Compare (control_dns_cache)

  • Status: failed
  • Exit code: 1
  • Base ref: origin/main
  • Head ref: f320411f0e63f5fae7a44c2459a09e3880a9f41e
  • Base strategy: merge-base
  • Package: ./control

Failure Log (tail)

[dns-bench] base ref: origin/main (49cc2160e66d8287c22513c38ce5dc00c02e4905)
[dns-bench] head ref: f320411f0e63f5fae7a44c2459a09e3880a9f41e (f320411f0e63f5fae7a44c2459a09e3880a9f41e)
Preparing worktree (detached HEAD 49cc216)
Preparing worktree (detached HEAD f320411)
go: downloading github.com/cilium/ebpf v0.20.0
go: downloading golang.org/x/sys v0.41.0
Error: flag provided but not defined: -strip-debug
exit status 1
control/control.go:8: running "go": exit status 1

component_upstream_hotpath

DNS Benchmark Compare

  • Base: origin/main (49cc2160e66d8287c22513c38ce5dc00c02e4905)
  • Head: f320411f0e63f5fae7a44c2459a09e3880a9f41e (f320411f0e63f5fae7a44c2459a09e3880a9f41e)
  • Package: ./component/dns
  • Benchmark filter: ^BenchmarkUpstreamResolver_GetUpstream_(Serial|Parallel)$
  • Common benchmarks: 2
  • Head-only benchmarks: 0

benchstat

goos: linux
goarch: amd64
pkg: github.com/daeuniverse/dae/component/dns
cpu: AMD EPYC 7763 64-Core Processor                
                                        │ /home/runner/work/dae/dae/bench-artifacts/component_upstream_hotpath/base_common.txt │ /home/runner/work/dae/dae/bench-artifacts/component_upstream_hotpath/head_common.txt │
                                        │                                        sec/op                                        │                            sec/op                             vs base                │
UpstreamResolver_GetUpstream_Serial-4                                                                             2.810n ± ∞ ¹                                                   2.809n ± ∞ ¹       ~ (p=0.500 n=3) ²
UpstreamResolver_GetUpstream_Parallel-4                                                                           9.171n ± ∞ ¹                                                   9.138n ± ∞ ¹       ~ (p=0.700 n=3) ²
geomean                                                                                                           5.076n                                                         5.066n        -0.20%
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

                                        │ /home/runner/work/dae/dae/bench-artifacts/component_upstream_hotpath/base_common.txt │ /home/runner/work/dae/dae/bench-artifacts/component_upstream_hotpath/head_common.txt │
                                        │                                         B/op                                         │                             B/op                              vs base                │
UpstreamResolver_GetUpstream_Serial-4                                                                              0.000 ± ∞ ¹                                                    0.000 ± ∞ ¹       ~ (p=1.000 n=3) ²
UpstreamResolver_GetUpstream_Parallel-4                                                                            0.000 ± ∞ ¹                                                    0.000 ± ∞ ¹       ~ (p=1.000 n=3) ²
geomean                                                                                                                      ³                                                                 +0.00%               ³
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal
³ summaries must be >0 to compute geomean

                                        │ /home/runner/work/dae/dae/bench-artifacts/component_upstream_hotpath/base_common.txt │ /home/runner/work/dae/dae/bench-artifacts/component_upstream_hotpath/head_common.txt │
                                        │                                      allocs/op                                       │                          allocs/op                            vs base                │
UpstreamResolver_GetUpstream_Serial-4                                                                              0.000 ± ∞ ¹                                                    0.000 ± ∞ ¹       ~ (p=1.000 n=3) ²
UpstreamResolver_GetUpstream_Parallel-4                                                                            0.000 ± ∞ ¹                                                    0.000 ± ∞ ¹       ~ (p=1.000 n=3) ²
geomean                                                                                                                      ³                                                                 +0.00%               ³
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal
³ summaries must be >0 to compute geomean

@MaurUppi MaurUppi marked this pull request as ready for review June 24, 2026 10:17
Codex added 4 commits June 24, 2026 18:32
Audit P2 found that attempted_dialers was appended once per buffered payload, which mixed packet count with dialer-attempt count and could create false consecutive duplicates. This keeps non-consecutive repeats for churn detection while compressing only consecutive duplicates from the same selected endpoint.

Constraint: Fix stays inside B-2 observability; retry selection, thresholds, and endpoint behavior remain unchanged.

Rejected: Fully de-duplicate the slice | non-consecutive repeats such as dead-a, dead-b, dead-a are the B-1 signal we need to preserve.

Confidence: high

Scope-risk: narrow

Tested: GOWORK=off GOCACHE=/tmp/dae-healthcheck-gocache go test control/addr.go control/udp_retry_diagnostics.go control/udp_retry_diagnostics_test.go -count=1

Tested: python3 -m unittest scripts/test_issue_1026_evidence_scripts.py

Tested: GOWORK=off GOOS=linux GOARCH=amd64 GOCACHE=/tmp/dae-healthcheck-gocache go test -exec /usr/bin/true ./component/outbound/dialer -count=1

Not-tested: Full Linux runtime/eBPF integration; PR CI will rerun after push.
The UDP retry evidence helper should be runnable directly on a target host without first exporting a log file. The CLI now reads dae.service through journalctl by default and treats a bare timestamp argument as the journal --since value, while retaining explicit --file support for saved logs and tests.

Constraint: Keep subprocess invocation shell-free and pinned to dae.service for this evidence script.

Rejected: Keep positional logfile as the default | the requested operator flow is journal-first with bare time input.

Confidence: high

Scope-risk: narrow

Tested: python3 -m unittest scripts/test_issue_1026_evidence_scripts.py

Tested: python3 scripts/udp_retry_log_correlate.py --help

Tested: manual --file smoke test with one synthetic retry-limit JSON line

Not-tested: Live journalctl on production dae.service; user will run the script manually after deployment.
A proxy WriteTo may report the on-wire byte count including protocol
encapsulation overhead (shadowsocks AES-128-GCM + IPv4 = +39 bytes), so n
can legitimately exceed len(b). UDP datagrams are atomic; only n < len(b)
is a genuine short write. The old n != len(b) misread every over-count as
a short write, retiring healthy endpoints and exhausting the retry limit.

Evidence: 10655/10655 captured drops were exactly +39 over-writes with
retry exhausted and zero genuine under-writes.
healthcheck_metrics_snapshot.py: replace the wrong N×CI stall threshold
with two independent detectors -- BLOCK-A (loop frozen: inflight==0 +
loopAdvancedAge > CI + probe-phase ceiling) and BLOCK-B (hung probe,
phase-relative, marked untested pending a real inflight>0 capture). Add
raw per-dialer time-series output (--raw-output) so single-vs-monotonic
is self-answerable. Replay: old N×CI flagged 3356/3356 healthy cycles;
calibrated BLOCK-A flags 0/3356.

udp_retry_log_correlate.py: parse enriched warning JSON, journald
short-iso timestamps. Tests 8/8.
@MaurUppi

Copy link
Copy Markdown
Owner Author

Superseded. Root causes for both daeuniverse#1026 and daeuniverse#1029 are now identified:

This instrumentation branch (fix/healthcheck-retry-evidence-1026-1029) was diagnostic scaffolding and is kept as an archived toolkit, not deleted. The minimal, no-instrumentation correctness fix is merged via the clean branch fix/udp-short-write-1029 instead.

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