Skip to content

Fix hub subscriber race, SOURCE_CHAN_SIZE handling, skipped-block serving, retry count and Range.Equals#58

Draft
sduchesneau wants to merge 5 commits into
developfrom
fix/hub-forkable-misc-bugs
Draft

Fix hub subscriber race, SOURCE_CHAN_SIZE handling, skipped-block serving, retry count and Range.Equals#58
sduchesneau wants to merge 5 commits into
developfrom
fix/hub-forkable-misc-bugs

Conversation

@sduchesneau

Copy link
Copy Markdown
Contributor

Fixes

Five independent correctness bugs, one commit each (branch fix/hub-forkable-misc-bugs off develop):

  1. Protect hub subscribers slice with a mutex (hub/hub.go)
    All SourceFrom* paths call subscribe under the forkable read lock only, while broadcastBlock/unsubscribe run under the write lock. Two concurrent subscribers raced on the h.subscribers append and one subscription could be silently lost (client hangs, never receives blocks). Added a hub-level leaf mutex protecting the slice in subscribe, unsubscribe, broadcastBlock and the shutdown callback; lock ordering is safe because it is acquired last and never held while calling out.

  2. Keep default SOURCE_CHAN_SIZE on invalid value (hub/hub.go)
    An unparseable SOURCE_CHAN_SIZE logged "ignoring" but assigned the zero value anyway, giving every subscription channel capacity 0 and disconnecting all clients with ErrSubscriptionChannelFull. Parsing is now in sourceChanSizeFromEnv, which falls back to the default on invalid input.

  3. Start blocksFromNum at first block >= num (forkable/forkable.go)
    Chains like Solana/NEAR skip block numbers; requiring an exact match made the hub unable to serve a request starting at a skipped number, stalling consumers on the file source. Now consistent with blocksFromNumWithForks (>= semantics). Requests below the lowest held block still error so callers fall back to another source instead of silently skipping blocks.

  4. Fix double attempt increment in retryable getter (tracker.go)
    RetryableBlockRefGetter incremented attempt both in the loop post statement and in the error branch, halving the configured retries. Removed the extra increment and made attempts mean the total number of tries (attempts=4 → exactly 4 calls); attempts == -1 (infinite) unchanged.

  5. Compare Range end blocks by value in Equals (range.go)
    endBlock was compared by pointer, so bounded ranges built separately never compared equal and IsNext always returned false for bounded ranges. Now compares by value (both nil, or both non-nil and equal).

Tests

  • hub/hub_subscribe_race_test.go: 20 concurrent subscribers while blocks broadcast; pre-fix trips -race (multiple DATA RACE warnings) and times out on a lost subscription; post-fix clean.
  • hub/source_chan_size_test.go: t.Setenv table for unset/empty/garbage/valid values + end-to-end NewForkableHub check (pre-fix: channel size 0).
  • forkable/blocks_from_num_test.go: buffer 3,4,6,7 (5 skipped); request 5 → serves 6,7; exact request unchanged; below-lowest and above-head still error.
  • tracker_test.go: counting fake getter; attempts=4 → exactly 4 tries; early-success and canceled-context cases.
  • range_test.go: Equals/IsNext tables including equal-value bounded ranges with distinct pointers.

Full suite: go test ./... passes. go test -race ./... passes for all changed packages; the only -race failures are pre-existing data races in test code of bstream (eternalsource/filesource/joiningsource tests) and blockstream (source_test.go), reproduced identically on a clean develop checkout.

🤖 Generated with Claude Code

An unparseable value was assigned anyway, leaving every
subscription channel with capacity 0 and disconnecting all
clients with ErrSubscriptionChannelFull.
subscribe runs under the forkable read lock only (all
SourceFrom* paths), so concurrent subscribers raced on the
slice and a subscription could be lost. The new mutex is a
leaf lock, acquired last and never while calling out.
Chains like Solana or NEAR skip block numbers; requiring an
exact match made the hub unable to serve a skipped start
block, stalling the consumer on the file source. Requests
below the lowest held block still fail so callers fall back
to another source.
The error path bumped attempt on top of the loop post
statement, halving the effective retries. Also make
'attempts' mean the total number of tries.
endBlock was compared by pointer, so bounded ranges built
separately never compared equal and IsNext always returned
false.
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.

1 participant