feat(scanner): add BlockListener with mpsc fan-in + reconnect#32
Merged
Conversation
) The bot's heartbeat. One `BlockListener` per configured chain subscribes to `newHeads` over WebSocket and forwards each block into a shared mpsc channel that the scanning pipeline will consume. - New `crates/charon-scanner/src/listener.rs`: - `ChainEvent::NewBlock { chain, number, timestamp }` — enum shape so future event kinds (protocol logs, oracle updates) land without changing channel types - `BlockListener::run` loops `run_once`, reconnecting with exponential backoff (1s → 30s cap) on any subscription/transport error - Per-block structured log: `chain=<name> block=<n> timestamp=<t>` - Clean shutdown when the receiver drops - CLI `listen` subcommand now: - Spawns a listener per `[chain.<name>]` in config - Drains the shared channel at `debug!` level - Exits on Ctrl-C or when all listeners terminate - Adds `futures-util` as a workspace dep for `SubscriptionStream::next()` Verified against BSC mainnet: `cargo run -- listen` streamed 304 blocks in 2m16s with zero warnings — subscription stable, reconnect logic dormant (no drops seen).
This was referenced Apr 22, 2026
…et supervision, metrics
- Reconnect backoff now adds 0-25% random jitter before each sleep to
avoid correlated retry storms against a single RPC endpoint when
many listeners disconnect at the same instant.
- BlockListener::publish uses try_send instead of a blocking await on
the mpsc sender; a full channel drops the block with a warn log and
a charon_listener_dropped_events_total counter increment, keeping the
WS drain loop responsive so the transport never buffers past its
server-side limit.
- Track last_seen block per chain. On reconnect, fetch the current head
and backfill every block between last_seen + 1 and head - 1 via
get_block_by_number, emitting ChainEvent::NewBlock { backfill: true }
so downstream consumers see the same heartbeat during disconnect
windows.
- CLI run_listen now spawns listeners into a tokio::task::JoinSet and a
supervise() helper drains join results, logging per-chain task panics
or errors and triggering shutdown when every listener exits. Ctrl-C
also aborts outstanding listeners.
- Per-block log downgraded from info to debug (BSC ~3 s = 28,800
info lines/day otherwise). Add charon_listener_connects_total,
charon_listener_disconnects_total, charon_blocks_received_total,
charon_listener_dropped_events_total counters for PR #50.
- Workspace adds rand and metrics deps; ChainEvent is #[non_exhaustive]
and carries a backfill flag.
Closes #92 #93 #94 #95 #96
This was referenced Apr 23, 2026
Closed
Closed
# Conflicts: # crates/charon-cli/src/main.rs # crates/charon-scanner/Cargo.toml # crates/charon-scanner/src/lib.rs
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.
Closes #8
The bot's heartbeat. One
BlockListenerper configured chain subscribes tonewHeadsover WebSocket and forwards each block into a shared mpsc channel the scanning pipeline consumes.ChainEvent::NewBlock { chain, number, timestamp }enum — shape admits future event kinds (protocol logs, oracle updates) without changing channel typesBlockListener::runloopsrun_once, reconnecting with exponential backoff (1s → 30s cap) on any subscription/transport errorchain=<name> block=<n> timestamp=<t>listenspawns one listener per[chain.<name>], drains the shared channel, exits on Ctrl-C or when all listeners terminatefutures-utilas a workspace dep forSubscriptionStream::next()Live-verified on BSC:
cargo run -- listenstreamed 304 blocks in 2m16s, zero warnings.Depends on #7 (
feat/06-chainprovider-ws).