device/telemetry: keep probing when the epoch fetch fails - #4143
Open
elitegreg wants to merge 5 commits into
Open
device/telemetry: keep probing when the epoch fetch fails#4143elitegreg wants to merge 5 commits into
elitegreg wants to merge 5 commits into
Conversation
The pinger fetched the current epoch before every tick and returned early on failure, so one unreachable ledger RPC endpoint stopped all TWAMP probing. Probing is pure UDP; the epoch only builds the sample buffer's partition key. During the 2026-07-29 outage this cost up to 19 hours of latency samples per device across 23 mainnet-beta devices. Cache the last known epoch and refresh it on its own loop, so the probe path never blocks on RPC. A failing fetch burns ~130s across its retries and the probe ticker only buffers one tick, so an inline fetch also swallowed roughly a dozen probe opportunities per failure. Probing is refused only when no epoch has ever been fetched, or when the cached one is older than -max-epoch-staleness (default 24h), past which a rollover is likely enough that samples would be misattributed to the previous epoch's account. Both cases log once rather than per tick, and repeated fetch failures collapse into fresh/stale transitions. Fixes #4125
elitegreg
marked this pull request as ready for review
August 3, 2026 14:41
elitegreg
enabled auto-merge (squash)
August 3, 2026 14:41
Contributor
There was a problem hiding this comment.
Pull request overview
Improves resilience of the device telemetry agent’s TWAMP probe loop by decoupling epoch fetching from the probe tick path and falling back to a cached epoch during ledger RPC outages, preventing probe gaps like the 2026-07-29 incident.
Changes:
- Cache the last known epoch in
Pingerand keep probing during epoch fetch failures, with a configurable staleness bound and log-once transitions. - Add a background epoch refresh loop plus new metrics/error typing to make epoch-cache health observable.
- Add targeted subtests covering fallback, recovery, refusal cases, staleness cutoff, and non-blocking behavior under a hung epoch fetch.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| controlplane/telemetry/internal/telemetry/pinger.go | Adds epoch cache + refresh loop; probes use cached epoch and refuse only when unavailable/too stale. |
| controlplane/telemetry/internal/telemetry/pinger_test.go | Adds subtests validating fallback, recovery, staleness refusal, log coalescing, and non-blocking probe cadence. |
| controlplane/telemetry/internal/telemetry/config.go | Introduces MaxEpochStaleness config with defaulting in validation. |
| controlplane/telemetry/internal/telemetry/collector.go | Plumbs MaxEpochStaleness and NowFunc into PingerConfig. |
| controlplane/telemetry/internal/metrics/metrics.go | Adds epoch-cache staleness gauge and pinger_epoch_unavailable error type. |
| controlplane/telemetry/cmd/telemetry/main.go | Adds -max-epoch-staleness flag wired into telemetry config. |
| CHANGELOG.md | Documents the behavioral change and new flag in Unreleased notes. |
Resolves: #4128 Independent of #4144 and #4145 (different file), so this one branches from `main`. ## Summary of Changes - `ledgerPeerDiscovery.refresh` no longer empties the peer cache before doing work that can fail. It cleared `p.peers` under the lock and then called `LocalNet.Interfaces()`, so a transient failure there returned with zero peers and `Pinger.Tick` iterated an empty slice, probing nothing until a later refresh succeeded. - The cache is now replaced only once the new list is built, and the lock covers just that assignment rather than the whole build. The clear was redundant with the existing assignment at the end of the happy path. - Success path is unchanged. ## Diff Breakdown | Category | Files | Lines (+/-) | Net | |------------|-------|-------------|------| | Tests | 1 | +79 / -0 | +79 | | Core logic | 1 | +6 / -5 | +1 | | Docs | 1 | +3 / -0 | +3 | | **Total** | 3 | +88 / -5 | +83 | A one-line behavioral fix plus the regression test that pins it. <details> <summary>Key files (click to expand)</summary> - [`controlplane/telemetry/internal/telemetry/peers.go`](https://github.com/malbeclabs/doublezero/pull/4146/files#diff-9c369dff3cb79259b8bc34d8d952b923103baeef1515402614c23a997a06c286) — drops the `p.peers = make(...)` clear, moves the mutex to wrap only `p.peers = peers`, and leaves a comment that nothing in the build may clear the cache </details> ## Testing Verification - New test lets the first refresh discover a peer, then fails every subsequent `LocalNet.Interfaces()` call, and asserts `GetPeers()` still returns the fully populated peer (link, device, tunnel, TWAMP port) after at least three failed refreshes. It fails against the pre-fix code, which returns an empty list. - Existing peer discovery tests pass unchanged, covering the success path and the skip cases. - Package passes under `-race`, since the change moves what the mutex covers.
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.
Summary of Changes
Pinger.Tickfetched the current epoch first and returned early on failure, so one unreachable ledger RPC endpoint stopped all TWAMP probing. Probing is pure UDP and needs no ledger access — the epoch only builds the sample buffer'sPartitionKey. During the 2026-07-29 outage (#4130) this cost up to 19 hours of latency samples per device across 23 mainnet-beta devices, for probes that would have succeeded the whole time.time.Tickerbuffers only one tick, so an inline fetch swallowed roughly a dozen probe opportunities per failure. A cached fallback alone would have resumed probing at ~1/13th the sample rate. The refresh cadence defaults to-probe-interval, so the epoch RPC rate is unchanged.-max-epoch-staleness(default 12h). The bound is load-bearing: contrary to the issue text, stale-epoch samples across a rollover are not discarded —Submitter.Tickremoves a past-epoch partition only when it is empty, and the program never validates the epoch against the clock (it is PDA seed material only). They land in the previous epoch's account, where the lake's derivedevent_tsplaces them past the real epoch boundary.getCurrentEpochdrops to Debug — at a 10s cadence it emitted thousands of lines across the outage, which is what [TRACKER] Telemetry outage 2026-07-29 — ledger RPC wedge and agent resilience #4130's "single-digit log lines per component" exit criterion is about.pinger_epoch_unavailableerror type and adoublezero_device_telemetry_agent_epoch_cache_stale_age_secondsgauge, matching the shape of the existingCachingFetcherstaleness metric.Testing Verification
-race: probing continues under the cached epoch while the fetch fails; the fallback and recovery each log exactly once; probing stops pastMaxEpochStalenesswith an injected clock; nothing is recorded and one error is logged when no epoch was ever fetched; and the probe loop holds its cadence while the epoch fetch hangs forever (the coalescing regression).epochForTickback to the pre-fix inline-fetch-and-give-up behavior: four of the five failed. The recovery test initially passed under the mutation because pre-outage samples satisfied it, so it now asserts the stale partition keeps growing while the fetch is failing.Run-driven tests.internal/netns/TestRunInNamespace_EmptyNameErrorsfails identically on a cleanmain(needs privileges forsetns); unrelated to this change.