Skip to content

Commit 558ff24

Browse files
authored
client: increase default latency probe interval from 30s to 5m (#3532)
## Summary of Changes - Increase the default `-probe-interval` for `doublezerod` from 30s to 300s (5 minutes), reducing steady-state ICMP control-plane load on DZDs by ~10x. With ~1700 clients each pinging ~90 devices, the old 30s interval contributed ~170+ ICMP pps per device — enough to compete with TWAMP telemetry packets under Arista COPP rate-limiting, causing spurious asymmetric link-down events. - When the first probe finds no reachable devices (e.g. daemon starts before network is ready), retry at a fast interval (min of configured interval and 30s) until a device responds, then switch to the steady-state interval. The first probe still fires immediately on startup, and `probeReady` is set unconditionally after the first pass so the CLI can proceed. ## Diff Breakdown | Category | Files | Lines (+/-) | Net | |--------------|-------|-------------|-------| | Core logic | 2 | +25 / -2 | +23 | | Tests | 1 | +79 / -0 | +79 | | Docs | 1 | +5 / -0 | +5 | | Generated | 1 | +2 / -0 | +2 | Mostly test coverage for the new fast-retry behavior; core logic change is compact. <details> <summary>Key files (click to expand)</summary> - [`client/doublezerod/internal/latency/manager_test.go`](https://github.com/malbeclabs/doublezero/pull/3532/files#diff-1e1223e68dfc0889b16b7cb9e53ec168040a8d4451fe536972f662abcf36555f) — new test verifying fast-retry when first probe finds no reachable devices, and that probeReady is set regardless - [`client/doublezerod/internal/latency/manager.go`](https://github.com/malbeclabs/doublezero/pull/3532/files#diff-5143bb1dcfda29287bc188c5574b0e0ee849b79fc690f0fb4a95dacdf3713eb9) — add `converged` flag and `hasReachable()` check to control fast/slow probe interval; add `maxInitialProbeInterval` constant - [`client/doublezerod/cmd/doublezerod/main.go`](https://github.com/malbeclabs/doublezero/pull/3532/files#diff-dee5a14f6d31f63f5de844ef5718568970a7d72d34482ba90b2dbeccd9032eb7) — change `-probe-interval` default from 30 to 300 </details> ## Testing Verification - New unit test `TestLatencyManager_FastRetryWhenUnreachable` verifies: probeReady is set after first probe even when all unreachable; second probe fires at the fast interval (~30s) rather than the steady-state interval (1h in test); probe count advances as expected - All existing latency manager tests pass (14/14) - E2E tests unaffected — client entrypoint hardcodes `-probe-interval 5` - Operators can still override with `-probe-interval <seconds>`
1 parent fb9d996 commit 558ff24

5 files changed

Lines changed: 111 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ All notable changes to this project will be documented in this file.
2020
- Add optional result destination to `GeolocationUser` so LocationOffsets can be sent to an alternate endpoint instead of the target IP; supports both IP and domain destinations (e.g., `185.199.108.1:9000` or `results.example.com:9000`); includes `SetResultDestination` onchain instruction, CLI `user set-result-destination` command, and Go SDK deserialization (backwards-compatible with existing accounts)
2121
- CLI
2222
- Add `--owner` flag to `multicast group update`, accepting a pubkey or `me` ([#3527](https://github.com/malbeclabs/doublezero/pull/3527))
23+
<<<<<<< HEAD
2324
- Polish terminal output of `connect` and `disconnect`: fix emoji semantics, normalize message phrasing across IBRL and multicast code paths, resolve tenant to human-readable code on connect (errors if tenant not found), and fix progress bar not clearing before output in `disconnect` ([#3529](https://github.com/malbeclabs/doublezero/pull/3529))
25+
=======
26+
- Client
27+
- Reduce default probing interval to 5m from 30s since DZDs don't generally move.
28+
>>>>>>> 413643f70 (client: increase default latency probe interval from 30s to 5m)
2429
2530
## [v0.17.0](https://github.com/malbeclabs/doublezero/compare/client/v0.16.0...client/v0.17.0) - 2026-04-10
2631

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/doublezerod/cmd/doublezerod/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var (
2929
env = flag.String("env", config.EnvTestnet, "environment to use")
3030
programId = flag.String("program-id", "", "override smartcontract program id to monitor")
3131
rpcEndpoint = flag.String("solana-rpc-endpoint", "", "override solana rpc endpoint url")
32-
probeInterval = flag.Int("probe-interval", 30, "latency probe interval in seconds")
32+
probeInterval = flag.Int("probe-interval", 300, "latency probe interval in seconds")
3333
cacheUpdateInterval = flag.Int("cache-update-interval", 30, "latency cache update interval in seconds")
3434
enableVerboseLogging = flag.Bool("v", false, "enables verbose logging")
3535
enableLatencyMetrics = flag.Bool("enable-latency-metrics", false, "enables latency metrics")

client/doublezerod/internal/latency/manager.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
const (
2020
serviceabilityProgramDataFetchTimeout = 20 * time.Second
21+
maxInitialProbeInterval = 30 * time.Second
2122
)
2223

2324
// DeviceInfo contains the minimal device information needed for latency probing and reporting.
@@ -424,13 +425,35 @@ func (l *LatencyManager) Start(ctx context.Context) error {
424425
probe()
425426
l.probeReady.Store(true)
426427

427-
ticker := time.NewTicker(l.probeInterval)
428+
hasReachable := func() bool {
429+
l.ResultsCache.Lock.RLock()
430+
defer l.ResultsCache.Lock.RUnlock()
431+
for _, r := range l.ResultsCache.Results {
432+
if r.Reachable {
433+
return true
434+
}
435+
}
436+
return false
437+
}
438+
439+
// If no device was reachable on the first probe, use a fast interval
440+
// until one responds, then switch to the steady-state interval.
441+
converged := hasReachable()
442+
interval := l.probeInterval
443+
if !converged {
444+
interval = min(l.probeInterval, maxInitialProbeInterval)
445+
}
446+
ticker := time.NewTicker(interval)
428447
for {
429448
select {
430449
case <-ctx.Done():
431450
return
432451
case <-ticker.C:
433452
probe()
453+
if !converged && hasReachable() {
454+
converged = true
455+
ticker.Reset(l.probeInterval)
456+
}
434457
}
435458
}
436459
}()

client/doublezerod/internal/latency/manager_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"runtime"
1313
"sync"
14+
"sync/atomic"
1415
"testing"
1516
"time"
1617

@@ -1574,6 +1575,84 @@ func TestLatencyManager_ProbeWaitsForDeviceFetch(t *testing.T) {
15741575
}
15751576
}
15761577

1578+
// TestLatencyManager_FastRetryWhenUnreachable verifies that when the first probe
1579+
// finds no reachable devices, the manager retries at a fast interval (<=30s)
1580+
// rather than the configured steady-state interval. probeReady is set after the
1581+
// first probe regardless, so the CLI can proceed with unreachable results.
1582+
func TestLatencyManager_FastRetryWhenUnreachable(t *testing.T) {
1583+
var probeCount atomic.Int32
1584+
1585+
mockSmartContractFunc := func(ctx context.Context) (*latency.ContractData, error) {
1586+
return &latency.ContractData{
1587+
Devices: []serviceability.Device{
1588+
{
1589+
AccountType: serviceability.DeviceType,
1590+
PublicIp: [4]uint8{192, 0, 2, 1},
1591+
PubKey: [32]byte{1},
1592+
Code: "dev01",
1593+
},
1594+
},
1595+
}, nil
1596+
}
1597+
1598+
mockProber := func(ctx context.Context, target latency.ProbeTarget) latency.LatencyResult {
1599+
n := probeCount.Add(1)
1600+
// First probe: unreachable. Second probe onwards: reachable.
1601+
return latency.LatencyResult{
1602+
Device: target.Device,
1603+
IP: target.IP,
1604+
Reachable: n >= 2,
1605+
}
1606+
}
1607+
1608+
manager := latency.NewLatencyManager(
1609+
latency.WithSmartContractFunc(mockSmartContractFunc),
1610+
latency.WithProberFunc(mockProber),
1611+
latency.WithProbeInterval(time.Hour), // large so only the fast retry fires
1612+
latency.WithCacheUpdateInterval(time.Hour), // don't refetch devices
1613+
)
1614+
1615+
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
1616+
defer cancel()
1617+
1618+
go func() {
1619+
_ = manager.Start(ctx)
1620+
}()
1621+
1622+
// Wait for the first probe to complete.
1623+
deadline := time.Now().Add(2 * time.Second)
1624+
for time.Now().Before(deadline) {
1625+
if probeCount.Load() >= 1 {
1626+
break
1627+
}
1628+
time.Sleep(10 * time.Millisecond)
1629+
}
1630+
if probeCount.Load() < 1 {
1631+
t.Fatal("timed out waiting for first probe")
1632+
}
1633+
time.Sleep(50 * time.Millisecond)
1634+
1635+
// probeReady should be true even though nothing was reachable — the daemon
1636+
// has completed a probe pass and the CLI needs to know it can read results.
1637+
if !manager.IsProbeReady() {
1638+
t.Fatal("expected probeReady=true after first probe")
1639+
}
1640+
1641+
// The manager should retry quickly (<=30s) rather than waiting the full
1642+
// probe interval (1h). Wait for the second probe with a generous timeout
1643+
// that is still well below the steady-state interval.
1644+
deadline = time.Now().Add(90 * time.Second)
1645+
for time.Now().Before(deadline) {
1646+
if probeCount.Load() >= 2 {
1647+
break
1648+
}
1649+
time.Sleep(10 * time.Millisecond)
1650+
}
1651+
if probeCount.Load() < 2 {
1652+
t.Fatal("timed out waiting for second probe — fast retry interval may not be working")
1653+
}
1654+
}
1655+
15771656
func TestUdpPing_ErrorCases(t *testing.T) {
15781657
tests := []struct {
15791658
name string

0 commit comments

Comments
 (0)