Skip to content

feat(scanner): add BlockListener with mpsc fan-in + reconnect#32

Merged
obchain merged 3 commits into
mainfrom
feat/07-block-listener
Apr 24, 2026
Merged

feat(scanner): add BlockListener with mpsc fan-in + reconnect#32
obchain merged 3 commits into
mainfrom
feat/07-block-listener

Conversation

@obchain
Copy link
Copy Markdown
Owner

@obchain obchain commented Apr 21, 2026

Closes #8

The bot's heartbeat. One BlockListener per configured chain subscribes to newHeads over 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 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 spawns one listener per [chain.<name>], drains the shared channel, exits on Ctrl-C or when all listeners terminate
  • Adds futures-util as a workspace dep for SubscriptionStream::next()

Live-verified on BSC: cargo run -- listen streamed 304 blocks in 2m16s, zero warnings.

Depends on #7 (feat/06-chainprovider-ws).

)

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).
…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
# Conflicts:
#	crates/charon-cli/src/main.rs
#	crates/charon-scanner/Cargo.toml
#	crates/charon-scanner/src/lib.rs
@obchain obchain merged commit 6e3862e into main Apr 24, 2026
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.

[scanner] Block listener: subscribe_blocks + structured per-block logging

1 participant