Skip to content

fix: comprehensive per-progress heartbeat coverage audit across worker/indexer long operations (#229)#244

Merged
tayy-i merged 12 commits into
mainfrom
fix/229-heartbeat-coverage-audit
Jul 23, 2026
Merged

fix: comprehensive per-progress heartbeat coverage audit across worker/indexer long operations (#229)#244
tayy-i merged 12 commits into
mainfrom
fix/229-heartbeat-coverage-audit

Conversation

@tayy-i

@tayy-i tayy-i commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Closes #229 — the systematic sweep ending the one-more-un-instrumented-op-per-review-round whack-a-mole. Enumerates every worker + indexer main/spawned-loop operation that can plausibly exceed the 20s heartbeat max-age at mainnet scale, and gives each a disposition: covered (with where), newly-covered (this PR), cannot-exceed-20s (with the scale reasoning), or n/a. The maintained coverage map lives in docs/storage.md's heartbeat section; one-shot operational commands (manual backfill/replay/rewind/repair) are dispositioned n/a as they run outside the registered service loops.

Newly instrumented surfaces (progress-tied beats only — never timers, preserving the false-green discipline): the canonical reconciliation tree, the resolver-profile-convergence tree, projection rebuild/hydration passes, the invalidation apply side, the backfill coordinator, and live-poll manifest/discovery refresh. Prior art (the untested 102-file sweep at 0c1e07c) was mined for locations; every change re-derived cleanly against current main with a beat-fires-during-long-op test per surface. All #213 false-green repro + wedge-stops-beating tests preserved.

Verify-loop found and fixed two real defects in-loop, both with regression tests: a shutdown path blocked by a held replay activity gate, and a deployment pool-minimum mismatch.

Gates

fmt, strict denying clippy, file-size gate (advisory warnings only), DB suites 2,121 passed / 0 failed (API 697, indexer 528, manifests 114, storage 498, worker 284), docs consistency check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p

@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: e500c79470

ℹ️ 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".

request_key,
requested_chain_positions,
topology_version_boundary,
record_version_boundary
FROM execution_cache_outcomes
WHERE request_type IN ('verified_resolution', 'verified_primary_name')
AND ($1::text IS NULL OR execution_cache_key > $1)
WHERE ($1::text IS NULL OR execution_cache_key > $1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep reorg invalidation scoped in SQL

When execution_cache_outcomes contains many rows for request types that reorg repair does not invalidate, such as declared_resolution, this query now pages the whole table and only skips those rows later in Rust. The removed request_type IN ('verified_resolution', 'verified_primary_name') predicate let the database discard out-of-scope rows before LIMIT, so each batch advanced through actual invalidation candidates instead of potentially many zero-work pages; please keep that predicate in SQL while still selecting request_type if needed for decoding.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed. R1 fixed in 7692b26 — the request_type predicate is restored in SQL (request_type still selected for decoding); a dominant-out-of-scope fixture asserts the expected batch progression.

return generic_topic_scan_source_identity_payload(source_plan);
}

if source_plan.selected_targets.len() <= COMPACT_SOURCE_IDENTITY_SELECTED_TARGET_THRESHOLD {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep non-source-family backfill identities stable

For whole-active or watched-target-set plans with more than 10,000 selected targets, this now falls through into the custom compact payload instead of delegating to WatchedSourceSelectorPlan::source_identity_payload(). That changes the persisted source_identity_hash (for whole-active plans it switches away from the shared FNV identity, and for watched-target sets it drops the full selected target list), so existing bootstrap checkpoints/backfill jobs won't be found and new jobs can be minted for already-covered work. Please keep this compaction limited to the source-family case and mirror that in the progress helper.

AGENTS.md reference: AGENTS.md:L8-L8

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed. R2 fixed in dea721e — compaction is again strictly source-family-scoped; whole-active and watched-target-set identity payloads/hashes are byte-stable vs origin/main (golden-hash tests: fnv1a64 whole-active, full-target-list watched sets), mirrored in the progress helper.

})?;
let parent_hash = block.parent_hash.clone();
let snapshots =
mark_chain_lineage_range_orphaned(pool, chain, &block_hash, parent_hash.as_deref())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep losing-branch orphaning atomic

When a reorg losing branch has more than one block, this loop now commits one-block chain_lineage orphaning transactions before the rest of the branch has been validated or updated. If a later parent lookup, orphan update, or heartbeat fails, the checkpoint still references the old head while one or more blocks on that path are already marked orphaned, leaving canonicality partially repaired; the previous range call loaded and updated the whole losing branch in one transaction. Please keep the branch canonicality update atomic, or validate the full path before doing per-block heartbeats.

AGENTS.md reference: AGENTS.md:L23-L23

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed. R3 fixed in 5a76ddf — the losing branch is validated (with per-block beats) first, then chain_lineage is updated in ONE transaction; an injected middle-block failure test leaves every block canonical, no partial repair.

@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: 5a76ddf4d1

ℹ️ 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".

let mut after_id = 0i64;
loop {
let ids = sqlx::query_scalar::<_, i64>(
"SELECT normalized_event_id FROM normalized_events WHERE normalized_event_id > $1 ORDER BY normalized_event_id LIMIT $2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Page only ENSv2 witness candidates

When a retained semantic witness check runs for one chain, this id page now advances through every normalized_events row in the database before the follow-up query narrows to ENSv2 registry raw-log events for that chain. On mainnet-sized tables this can turn each startup verification into a global normalized-event table walk with many zero-work pages; please include the chain/derivation/raw-fact/canonicality filters in the paged query before applying LIMIT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed — witnesses id page now carries the chain/derivation/raw-fact/canonicality filters before LIMIT (aeb2ccd). Additionally, a full sweep enumerated every paged query this branch introduced or restructured; each is now either fixed or verified to carry its scoping predicates before LIMIT (table in the PR record).

Comment on lines +135 to +139
SELECT discovery_edge_id
FROM discovery_edges
WHERE discovery_edge_id > $1
ORDER BY discovery_edge_id
LIMIT $2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope streamed deactivation pages to the source

When reconciling a single discovery source, this page source walks all discovery_edges rows and only the follow-up query filters to active rows for de.discovery_source = $1. On deployments with many edges from other sources, a source-specific full reconcile can spend most pages doing zero deactivation work before reaching its own candidates; please page the active rows for the requested source before applying LIMIT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed — streamed single-source deactivation now pages active rows for the requested discovery_source before LIMIT (aeb2ccd). Additionally, a full sweep enumerated every paged query this branch introduced or restructured; each is now either fixed or verified to carry its scoping predicates before LIMIT (table in the PR record).

source_scope: Option<&[(String, String, i64, i64)]>,
mode: PersistedRawPayloadAdapterSyncMode,
reconcile_full_source: bool,
progress: &mut Option<&mut dyn bigname_adapters::StartupAdapterProgress>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route ENSv2 registry block replays through progress

When a progress-enabled block-hash adapter sync reaches the ENSv2 registry outside the full-source/live-poll arms, this new progress handle is not used: the LiveOrBackfill arms below still call the non-progress sync_for_block_hashes* entrypoints. A large startup or post-bootstrap chunk can therefore actively page and persist ENSv2 registry work without refreshing the indexer service heartbeat until the adapter returns; please add and use progress-aware block-hash entrypoints when progress is present.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed — progress-aware block-hash entrypoints added and routed in the LiveOrBackfill arms when progress is present; the coverage map's disposition row updated (2389386). Additionally, a full sweep enumerated every paged query this branch introduced or restructured; each is now either fixed or verified to carry its scoping predicates before LIMIT (table in the PR record).

Comment on lines +17 to +19
let source_ids = sqlx::query_scalar::<_, i64>(&format!(
"SELECT {source_id_column} FROM {source_table} WHERE {source_id_column} > $1 ORDER BY {source_id_column} LIMIT $2"
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Page coverage candidates inside the watched scope

When stored-lineage coverage publication runs with progress, this page source first walks every row in contract_instance_addresses or discovery_edges, and only the follow-up insert filters to the requested chain, source families, and block range. On deployments with many unrelated addresses or edges, publishing one chain's coverage frontier can spend most pages doing no candidate work while repeatedly rebuilding watched CTEs for global pages; please apply the watched-scope filters before LIMIT.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed — coverage-frontier page sources now apply the watched chain/family/intersection scope before LIMIT (aeb2ccd). Additionally, a full sweep enumerated every paged query this branch introduced or restructured; each is now either fixed or verified to carry its scoping predicates before LIMIT (table in the PR record).

@tayy-i
tayy-i force-pushed the fix/229-heartbeat-coverage-audit branch from 2389386 to 5057793 Compare July 23, 2026 05:20

@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

let status = match recover_ens_v2_live_coverage_requirement(

P2 Badge Thread replay recovery through progress

When automatic normalized replay hits missing ENSv2 retained-history coverage, this branch still calls the non-progress recovery helper even though progress is in scope. That helper runs the hash-pinned provider backfill without run_resumable_hash_pinned_backfill_job_with_progress, so an exact recovery range large enough to take longer than the heartbeat max age can leave the spawned catch-up task’s service-loop heartbeat stale despite active work. Please route the Some(progress) case through recover_ens_v2_live_coverage_requirement_with_progress, as the live-poll recovery path does.

AGENTS.md reference: AGENTS.md:L7-L7

ℹ️ 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".

TateB and others added 10 commits July 23, 2026 09:24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Keep heartbeat reporting per candidate batch while restricting keyset pagination to verified resolution and verified primary-name outcomes in SQL.

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

Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Limit the progressive compact identity to source-family plans and retain the pre-existing whole-active and watched-target-set payloads and hashes while recording heartbeat progress.

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

Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Validate the losing lineage path with progress heartbeats before applying one transactional range update, so failures cannot leave mixed canonicality.

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

Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Apply operation scope before LIMIT across retained witnesses, discovery reconciliation, watched coverage, resolver convergence, and related audit sweep sites. Add dominated-table exact-page-count regressions and correct the exact-boundary requirement beat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Add progress-aware ENSv2 registry block-hash entrypoints, route every reconciliation mode through them when a heartbeat handle is present, and lock the coverage with an out-of-scope-dominated exact-beat regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Route completed bootstrap raw-range replay through the progress-aware normalized-event entrypoint and cover multi-page callbacks at the caller boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Restore origin/main's range-transaction boundaries for losing-branch payload families and winning-lineage re-canonicalization while retaining read-side and post-commit progress beats.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Use total emitter identity ordering after both watched-contract loader paths and restore the ordinary full-replay page size while retaining small startup heartbeat pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
@tayy-i
tayy-i force-pushed the fix/229-heartbeat-coverage-audit branch from 8f23b7e to 04c08fc Compare July 23, 2026 09:34

@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: 04c08fc92f

ℹ️ 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 on lines 425 to 428
WHERE desired.desired_row_id > $2
AND desired.active_from_block_number IS NOT NULL
AND EXISTS (
SELECT 1
FROM discovery_edges de
JOIN contract_instance_addresses cia
ON cia.contract_instance_id = de.to_contract_instance_id
AND cia.deactivated_at IS NULL
WHERE de.discovery_source = $1
AND de.deactivated_at IS NULL
AND de.provenance ->> 'observation_key' = desired.observation_key
AND de.chain_id = desired.chain_id
AND de.edge_kind = desired.edge_kind
AND de.from_contract_instance_id = desired.from_contract_instance_id
AND NOT {STREAMED_EDGE_IS_ORPHANED_SQL}
AND {STREAMED_STARTS_AFTER_SQL}
)
ORDER BY desired.desired_row_id
LIMIT $3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Page historical successors before limiting

When a streamed full-source reconcile has many staged desired edges but only a small retained-successor diff, this page now advances through every desired row with a start block and only the LATERAL query below decides whether any of those rows are historical. The previous successor EXISTS predicate was applied before LIMIT, so each page advanced through actual historical candidates; without it, a near-no-op finalize can spend most pages doing no historical work while holding the reconciliation transaction. Please put the successor condition back into the paged query, or page a materialized successor-candidate set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed. F1 fixed — the successor condition is restored into the paged query before LIMIT, with a page-progression test on a non-historical-dominated fixture.

provider: Option<&(impl ChainProviderOps + ?Sized)>,
header_audit_mode: HeaderAuditMode,
mut raw_log_input_version: RawLogStagingInputVersion,
progress: &mut Option<&mut NormalizedReplayHeartbeat>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route coverage recovery through progress

When automatic normalized replay hits missing ENSv2 coverage, this new progress handle is not propagated: the recovery branch below still calls the non-progress recover_ens_v2_live_coverage_requirement, which runs the hash-pinned recovery backfill without heartbeat callbacks. For a large generation-bound recovery interval, the required replay subtask can actively fetch and persist coverage while the shared indexer heartbeat goes stale; please call the _with_progress recovery path when progress is present.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dispositioned: confirmed and fixed. F2 fixed — the coverage-recovery branch now routes through the _with_progress recovery path when a progress handle is present, with a per-page beat test. The sweep was also re-run on the final tree (post-rebase) to close both classes again.

TateB and others added 2 commits July 23, 2026 10:51
Filter staged desired edges by a qualifying active successor before the page limit, and cover page progression with a non-historical-dominated fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
Route normalized replay ENSv2 recovery, scoped ENSv1 emitter conversion, and startup authority checkpoint deletion through their progress-aware paths. Add caller-level per-page coverage and scoped-emitter regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9RTnbCyhqEUSPc7xCK76p
@tayy-i
tayy-i merged commit f81006a into main Jul 23, 2026
10 checks passed
@tayy-i
tayy-i deleted the fix/229-heartbeat-coverage-audit branch July 23, 2026 11:14
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.

liveness: comprehensive per-progress heartbeat coverage audit across all worker/indexer long-ops

2 participants