Skip to content

Keep hub final blocks until they are merged - #92

Merged
sduchesneau merged 10 commits into
developfrom
feature/hub-keep-blocks-until-merged
Nov 13, 2026
Merged

sduchesneau merged 10 commits into
developfrom
feature/hub-keep-blocks-until-merged

Conversation

@sduchesneau

@sduchesneau sduchesneau commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

A hub keeps a fixed keepFinalBlocks below LIB. A merger further behind than that left a range of blocks that neither the merged-blocks files nor the hub could serve: a stream reading through the files reached the last one and waited there until the merger caught up.

With hub.WithMergedBlocksStore(store, bundleSize, maxKeptFinalBlocks), the merged-blocks store decides what the hub keeps, and keepFinalBlocks is passed as 0:

  • The hub keeps every final block from the start of the last merged-blocks file it found. That is up to two files worth when the merger keeps up.
  • At boot it finds where merged blocks end with one listing of the store, from maxKeptFinalBlocks below LIB.
  • It then finds new files with one FileExists call on the next expected file, at most every 5 seconds and only once LIB passed the end of that file. When LIB is two files or more past it, as when the merger catches up, it uses one listing instead of one call per file.
  • It starts from the last merged-blocks file when it boots, from one-block files or from live blocks.
  • Past maxKeptFinalBlocks it drops the oldest blocks anyway, and it logs a warning every 30 seconds while merged blocks are more than three files behind.
  • hub.WithMergedBlocksLagMetric reports the final blocks not found in a merged-blocks file, and ForkableHub.MergedBlocksUpTo returns the first of them.
  • bundleSize is the size of the files in that store (0 means bstream.DefaultMergedBlocksBundleSize), so a hub reading files of another size than the process default looks for the right files.

Cases around missing files and failures:

  • No merged-blocks file yet: nothing is dropped (up to the maximum), the hub never starts below the first streamable block, and the lag counts from that block.
  • Hub booting further above the last file than maxKeptFinalBlocks: it starts at its maximum depth and does not look for files below it. The lag metric reads about maxKeptFinalBlocks at first, and the hub picks up from the file at that depth once the merger writes it.
  • Failed lookup or listing: retried with the hub's bootstrap settings (3 attempts, 100ms apart). If it still fails at boot, the hub starts two files below LIB and drops nothing until a lookup succeeds.
  • Each search of the store stops after 30 seconds and when the hub shuts down, so a store call that hangs does not block the hub.
  • A hub booting from live blocks searches the store once and reuses the result for the following blocks. After a failed search it waits 5 seconds before searching again.
  • One-block files missing between the blocks the hub loaded and the live blocks: the hub stays not ready rather than start above the gap, since streams going from merged blocks to live blocks would miss those blocks. Every 30 seconds it logs which blocks it is waiting for, and it gets ready once the files show up.
  • Files written out of order: the hub takes the first run of consecutive files in its reach, so a file the merger wrote ahead of a lower one that is still missing is not taken for the end of merged blocks. Other names in the store (not a number, not on a file boundary) are ignored.

A hub built without the option behaves as before (the relayer uses that).

go test -race ./hub/ reports a data race in TestForkableHub_LinkLiveUsingOneBlocks_StopsAtLiveBlock, in that test's own store hook; it is there without this PR's last commit too.

A merger more than keepFinalBlocks behind LIB left a range of blocks
that neither the merged-blocks files nor the hub could serve, so streams
reading through the files waited there for the merger.
A hub following the merged-blocks store no longer also applies a fixed
depth below LIB, so there is one rule to reason about. The fixed depth
remains for hubs without the store.
The lag counts from the first streamable block until the first
merged-blocks file exists, and a hub booting further above the last
file than it can hold searches for where merged blocks really end.
The hub used the process-wide default, so a hub reading files of another size
looked for files that never exist and stopped following the merger.
Each live block the hub could not link searched the merged-blocks store again until the hub had a LIB. It now reuses the last result, and waits 5s after a failed search.
Below maxKeptFinalBlocks the search could miss files that do not start at the first streamable block, leaving the hub waiting on a file the merger never writes.
A hub waiting on missing one-block files now says which blocks it waits for. Merged-blocks lookups stop after 30s and on shutdown instead of hanging the hub.
A hub starting while the merger is behind looked up one file per call
going down from LIB, and one per call going up when the merger caught
up. Both are now one listing. The listing also shows a file written
ahead of a missing one, which is no longer taken for the end of merged
blocks.
@sduchesneau
sduchesneau marked this pull request as ready for review November 12, 2026 21:15
@dfuse-bot
dfuse-bot requested a review from billettc November 12, 2026 21:16
One appended to a slice from concurrent downloads. The other started
its consumer before pushing blocks, so which partial blocks got skipped
depended on timing.
@sduchesneau
sduchesneau requested review from UlysseCorbeil and removed request for billettc November 12, 2026 21:19
Comment thread hub/merged_blocks.go
}

ok = false
err = t.store.WalkFrom(ctx, "", mergedBlocksFilename(base), func(filename string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On Azure, WalkFrom lists the whole store from the first file, so with millions of merged files this times out and locate never succeeds. A prefix would bound it

I checked and its not an issue for dstore local disk

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good catch: when streamingfast/dstore#63 is merged, I will bump dstore here and it will fix the issue. (azure did NOT have the walkFrom feature when this was implemented, it does now.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

bumping dstore!

Comment thread hub/merged_blocks.go
// purgeLimit is a forkable.WithFinalBlocksPurgeLimit function. It keeps the blocks of the
// last merged-blocks file and everything above: a stream reading the files joins the hub
// on a block it received from a file, so the hub has to overlap the last one.
func (t *mergedBlocksTracker) purgeLimit(libNum, cutoff uint64) uint64 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking but if the merger is behind, a stream joining the hub gets every unmerged block at once and has 30s to get through them

So a slow consumer won't finish in time, it gets disconnected -> reconnects -> repeats

Should the blocks queued at join time not count toward the 30s?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

30s should be enough even with a few hundreds more blocks, and if the user doesn't consume fast enough, they will simply loop in the "retry" that will reevaluate the source from the cursor and keep going from there (without an actual disconnection) so:

  1. it is only a problem when an unusual situation (merger lags behind) happens
  2. it is only in the rare situation where the user will not be able to process/download those extra blocks within 30 seconds
  3. the impact is very limited and invisible to the user (creating a new source from last sent cursor) -- it will still eventually allow the user to catch up.

so no real problem here.

@UlysseCorbeil UlysseCorbeil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved with minor interrogation points

@sduchesneau
sduchesneau merged commit 719404b into develop Nov 13, 2026
3 checks passed
@sduchesneau
sduchesneau deleted the feature/hub-keep-blocks-until-merged branch November 13, 2026 02:01
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.

2 participants