Skip to content

feat(debug-trace-server): age-based witness routing, configurable old-block budget, tip buffer - #158

Merged
flyq merged 9 commits into
mainfrom
liquan/feat/witness-tiered-fetch
Jul 24, 2026
Merged

feat(debug-trace-server): age-based witness routing, configurable old-block budget, tip buffer#158
flyq merged 9 commits into
mainfrom
liquan/feat/witness-tiered-fetch

Conversation

@flyq

@flyq flyq commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Witness fetches in the debug-trace-server now route by block age: blocks at least --witness-local-window (default 4096, matching the witness generator's BACKUP retention) below the local DB tip skip the first witness endpoint — the internal generator, a guaranteed miss for anything it has pruned — and fetch straight from the fallback endpoints. Previously every historical request burned a doomed generator probe plus a failover round trip before reaching an endpoint that could actually serve it, and the hardcoded 3s old-block fail-fast was the dominant source of customer-visible -32001 timeouts.

Design

The historical route is a skip parameter on the shared witness path, not a second client: RpcClient::get_witness_light_with_deadline_from(skip, ..) (crates/stateless-common/src/rpc_client.rs:649) slices witness_providers[skip..] and its parallel endpoint labels into the existing retry loop. Attribution rides on the {idx}:{host} labels (endpoint_label, rpc_client.rs:899), which bake in each endpoint's index in the full configured list at construction, so sliced routes stay globally correct in logs and per-endpoint metrics; the numeric provider_idx field #156 added to the retry logs is dropped — under slicing the raw loop index would misattribute, and the label already carries the index. One client means one witness concurrency semaphore (the --witness-max-concurrent-requests cap stays global) and no duplicate connection pools.

Routing is derived per fetch, not configured: witness_route (bin/debug-trace-server/src/data_provider.rs:781) skips the generator when the block is historical and a fallback endpoint exists (witness_provider_count() >= 2, data_provider.rs:814), so the skip can never see an empty rotation. An unknown local tip counts as recent, so stateless mode (no --data-dir) never routes; the background chain-sync prefetch deliberately always uses the full chain (it fetches at the sync frontier, within the generator's retention except during deep catch-up with --blocks-to-keep beyond it — documented on TraceFetcher). Startup logs state which mode is in effect, identifying the skipped endpoint by its credential-stripped label via the new RpcClient::witness_provider_label (rpc_client.rs:359). The first --witness-endpoint is positionally the internal generator; README and AGENTS.md document the contract.

New knobs: --witness-old-block-timeout (main.rs:231) replaces the hardcoded 3s old-block fail-fast; when unset it tracks the effective --witness-timeout budget (raising the witness budget also raises the old-block cap), and explicit values are clamped to --witness-timeout. --witness-local-window (main.rs:225) sets the routing threshold; --tip-buffer (main.rs:238, default 2) keeps the chain-sync fetcher from racing the generator at the head. The tip_buffer < stale-reset-threshold invariant is enforced twice: validate_args (main.rs:301) gives flag-named CLI errors, and the new PipelineConfig::validate() (crates/stateless-core/src/pipeline/config.rs:51) — enforced at run_pipeline entry (pipeline/mod.rs:65) — protects every embedder that constructs PipelineConfig directly (the mega-reth FullNode sets stale_reset_threshold = None and is unaffected). Witness-fetch failure logs carry source/old_block/budget_ms/elapsed_ms for incident attribution.

Testing

The routing dispatch is pinned end-to-end in both directions: with two mock endpoints a historical block never touches the generator and a recent block probes it first; with a single endpoint a historical block is served by the sole endpoint without tripping the skip assert — a >= 1 regression of the fallback derivation now fails the suite (mutation-verified). A three-endpoint skip test asserts every recorded attempt keeps its original-index {idx}:{host} label, pinning the parallel-slicing property the per-endpoint metrics rely on. Unit tests cover the is_historical boundary (including overflow and zero-window), the old-block budget clamp and its tracking default (unset follows a raised --witness-timeout; an explicit flag wins), route/label selection, CLI+env parsing of all three knobs, and the tip-buffer validation at both the CLI and core levels; a previously-flaky tracing log-capture test was made robust to runner load and callsite-interest races. Full workspace cargo test, cargo fmt --check, cargo sort --check, and cargo clippy are clean.

Operational notes (dashboards & behavior changes)

  • source="witness_generator" no longer covers the whole RPC witness path: once routing is active, historical traffic records under source="witness_historical" in both DataSourceMetrics and WitnessSourceMetrics, and the RPC path is the sum of the two labels — rescope dashboards/alerts keyed on witness_generator (same class of change as perf: zero-validation light witness decode; switch debug-trace-server onto it #154/feat(metrics): per-endpoint RPC attempt metrics + timeout reasons #156).
  • Retry-loop logs drop the numeric provider_idx field introduced by feat(metrics): per-endpoint RPC attempt metrics + timeout reasons #156; provider={idx}:{host} is the attribution mechanism — migrate any tooling that greps the numeric field.
  • The old-block witness budget default moves from a hardcoded 3s to the full --witness-timeout budget (tracking it when unset); deployments that relied on the fail-fast should set --witness-old-block-timeout 3.
  • New startup validation rejects --tip-buffer >= --blocks-to-keep (the built-in lag would otherwise trip a stale-anchor reset on every transient restart).
  • With a single witness endpoint or no --data-dir, routing is inert (fetch order unchanged from main); a startup warn calls out the multi-endpoint-without-data-dir case.

Notes

🤖 Generated with Claude Code

flyq and others added 2 commits July 22, 2026 23:36
…-block budget, tip buffer

Witness fetches route by block age: blocks at least --witness-local-window (default 4096, matching the witness generator's BACKUP retention) below the local DB tip skip the first witness endpoint — the internal generator, a guaranteed miss for anything it has pruned — via a skip parameter on the shared RpcClient witness path: one client, one witness semaphore, and retry-loop provider indices stay aligned with the configured endpoint list. Routing requires two or more witness endpoints and --data-dir (the local tip anchors block age); otherwise all fetches use the full chain and startup logs say so.

The old-block witness budget (previously a hardcoded 3s fail-fast that was the dominant source of customer-visible -32001 timeouts) becomes --witness-old-block-timeout, defaulting to the full witness budget (8s). New --tip-buffer (default 2) keeps the chain-sync pipeline from racing the generator at the head, validated at startup to stay below --blocks-to-keep. Witness-fetch failure logs carry source/old_block/budget_ms/elapsed_ms for incident attribution, and the historical route gets its own witness_historical metrics label.

The routing dispatch is tested end-to-end against two counting mock endpoints, plus route/label selection, env parsing of the new knobs, and a skip-all panic guard. README/AGENTS document the positional first-endpoint contract and the new knobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iered-fetch

# Conflicts:
#	crates/stateless-common/src/rpc_client.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 569c41fcc8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread bin/debug-trace-server/src/main.rs Outdated
flyq and others added 2 commits July 23, 2026 00:21
…ite pressure

The stall-WARN capture test raced two ways: a fixed 1s wall-clock window around the whole fetch could expire before a loaded runner scheduled the first 50ms attempt timeout, and concurrent tests driving the same tracing callsites with no subscriber installed could race set_default's interest-cache rebuild, re-caching a stale `never` for the instrument span so every WARN lost its block_number field. The fetch now runs as a background task while the test polls the captured logs (finishing on the first span-carrying WARN, 30s ceiling), periodically rebuilding the interest cache and respawning the fetch so a clobbered cache self-heals. Reproduced 6/15 before, 0/30 after under --test-threads=2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… home tip-buffer invariant in core

Apply the /simplify review of PR #158. WitnessFetchConfig loses route_historical: fetch_witness derives the skip from the client's own endpoint count (the data-dir half was already implied — an unknown tip counts as recent), so the cross-crate "must only be set when..." invariant becomes unrepresentable and the config turns Copy. round_robin_with_backoff drops provider_idx_base: endpoint labels bake in their global index, so the numeric provider_idx duplicated the adjacent label in every log line. PipelineConfig::validate() homes the tip_buffer < stale_reset_threshold invariant in core where both fields live and run_pipeline enforces it for every embedder; the CLI check stays for flag-named errors. Also: DEFAULT_OLD_BLOCK_WITNESS_TIMEOUT_SECS structurally equals DEFAULT_WITNESS_TIMEOUT_SECS, an is_old_block helper unifies the twice-written predicate, record_block_distance reuses db_tip_height, DataProvider::new takes a Duration like the config it sits beside, the deflake test becomes two bounded loops with a single abort path, and the retention rationale collapses onto the DEFAULT_WITNESS_LOCAL_WINDOW doc instead of eight restatements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08c87271bf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread bin/debug-trace-server/src/main.rs Outdated
flyq and others added 2 commits July 23, 2026 09:28
…; log stripped endpoint label at startup

Address the remaining review findings on PR #158. Two coverage pins: a single-witness-endpoint e2e test proves a historical block fetches through the sole endpoint without tripping the skip assert (a >= 1 regression of the fallback derivation now fails the suite — mutation-verified), and a three-endpoint skip test asserts every recorded attempt keeps its original-index {idx}:{host} label (the parallel-slicing property the per-endpoint metrics rely on). The routing-active startup log now emits the credential-stripped label via the new RpcClient::witness_provider_label accessor instead of the raw configured URL, matching the crate's own label hygiene. Boundary prose unified onto fewer-than/at-least wording in the CLI help, module docs, and metric docs (the exactly-at-the-window block is historical, as the tests pin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex review on PR #158 correctly noted the routing contract reads broader than the implementation: the background chain-sync prefetch still uses the full endpoint chain, so during deep catch-up with --blocks-to-keep beyond the generator's retention each pruned block burns one generator probe before failover. That trade-off is intentional — the sync frontier normally trails the remote head by only tip_buffer, and routing there would need a remote-head anchor instead of the local tip — so scope the claims instead of the code: README, AGENTS.md, the module doc, and the --witness-local-window help now say routing applies to request serving, and TraceFetcher documents the accepted catch-up cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.1%. Comparing base (51c41b3) to head (f71a36a).
⚠️ Report is 1 commits behind head on main.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb6d6af1b6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread bin/debug-trace-server/src/main.rs Outdated
…-timeout

Address the Codex P2 on PR #158: --witness-old-block-timeout becomes optional and, when unset, tracks the effective --witness-timeout instead of a fixed 8s constant — an operator who raises the witness budget no longer leaves old blocks silently capped at 8s. An explicit value still wins and is clamped to --witness-timeout (now also in the startup log, so the logged budget always equals the effective one). The DEFAULT_OLD_BLOCK_WITNESS_TIMEOUT_SECS constant is deleted; the test helper mirrors the unset-flag semantics, and CLI tests pin both raised-budget tracking and explicit-flag-wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@vincent-k2026 vincent-k2026 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve — age-based witness routing is a solid reliability fix. No correctness/security/data-loss/liveness blockers.

Verified locally on the PR branch (HEAD 1e2086a, nightly-2026-02-03):

  • cargo test -p stateless-common -p stateless-core -p debug-trace-server → green, 0 failed. All new routing/skip/validation tests run and pass — incl. the single-endpoint >=2-fallback guard, the original-index label-alignment test under slicing, and the skip-all should_panic.
  • cargo fmt --check → clean.

The crux is right: using the local DB tip as a proxy for the generator's retention is conservative. Because the local sync trails the head (tip_buffer), local_tip <= gen_tip, so any block judged historical (block + local_window <= local_tip) is genuinely below the generator's BACKUP floor — skipping it can never drop a servable endpoint, and the reverse mis-classification only costs one redundant generator probe that failover covers. --witness-local-window is safe to mis-set either direction. Invariant is structurally enforced (have_fallback = count>=2 derivation + assert!(skip < len) at the choke point, should_panic-tested) and double-validated for tip_buffer (CLI validate_args + core PipelineConfig::validate()). Metrics pre-registered, doc mirrors updated, db_tip read once (no torn read).

Non-blocking:

  1. (worth confirming) The old-block witness budget default moves 3s → full witness budget (8s). The routing is the real -32001 fix; the budget default is a coupled change. Net effect: a witness pruned everywhere now takes up to 8s to surface -32001 (was 3s), and this applies to the within-window old class too (still generator-first). Intended? It's documented and mitigatable via --witness-old-block-timeout 3 — just confirming the default is deliberate.
  2. (minor) The success-path witness_historical metric attribution isn't pinned by a test — the mocks only error, so fetch_witness's Ok branch never runs. Low risk (witness_route's label is unit-tested); a follow-up assertion would close it.
  3. (minor) The deflaked test_witness_failure_log_carries_block_number has a ~30s worst-case self-healing ceiling, so a real span-field regression takes that long to go red. #[serial] isolation might be simpler. Follow-up.

LGTM.

Comment thread bin/debug-trace-server/src/main.rs Outdated
… generator convention

Address the design review comment on PR #158: the generator is a different kind of endpoint (prunes beyond BACKUP, probed first only for recent blocks), so encode that in the interface instead of in ordering. New optional --witness-generator-endpoint declares it explicitly; --witness-endpoint now lists only durable fallbacks (optional when the generator flag is set — generator-only works like any single-endpoint config). The combined chain always puts the generator at index 0, so the fetch-side skip derivation (count >= 2) and everything in data_provider/rpc_client are unchanged. Misordering becomes unrepresentable, and validate_args rejects the likely migration mistake of leaving the generator duplicated in the fallback list. Backward compat for one release: without the flag, the first of two or more --witness-endpoint values still acts as the generator, with a startup deprecation warning when routing is active. The startup config log now shows the combined effective chain. README/AGENTS.md updated; tests pin all chain shapes, the deprecation flag, generator-only parsing, the neither-flag rejection, and the duplicate guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd7cebcea6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread bin/debug-trace-server/src/main.rs
@flyq
flyq requested a review from Troublor July 23, 2026 07:06
Comment thread bin/debug-trace-server/src/main.rs Outdated
… via --witness-generator-endpoint only

Address the follow-up design review on PR #158: the positional convention never shipped, so there is no released behavior to stay compatible with, and keeping the inference would make the upgrade behavior-changing — a plain [durable-A, durable-B] failover pair on main would silently gain generator-skipping — while leaving "two durable endpoints, no generator" inexpressible. Now only a declared --witness-generator-endpoint is ever skipped; without it every witness endpoint is plain failover, exactly the pre-PR semantics, and routing is a deliberate opt-in.

The generator-first knowledge cannot be derived from the client (all endpoints speak the same witness RPC), so WitnessFetchConfig carries generator_first — unlike the derivable routing bool removed earlier, this is irreducible CLI knowledge, and the count-based guard at the use site still makes an empty rotation unrepresentable. The deprecation warning, the chain bool, and the deprecated-form paragraphs in README/AGENTS.md and both flag docs all disappear. A new e2e test pins that a generator-less failover pair never skips; the single-endpoint test now pins the fallback guard with a declared generator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@flyq
flyq requested a review from Troublor July 23, 2026 13:28
@flyq
flyq merged commit 80cfb0e into main Jul 24, 2026
16 of 17 checks passed
@flyq
flyq deleted the liquan/feat/witness-tiered-fetch branch July 24, 2026 04:11
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.

3 participants