fix(sync): record snap progress per backend - #12857
Conversation
|
Claude finished @batrr's task in 9m 41s —— View job Review:
|
| # | Severity | Where | Note |
|---|---|---|---|
| 1 | Low | ISnapTrieFactory.cs:20-21 |
New members are abstract; default impls (=> false / { }) would match the documented semantics, drop the flat/test boilerplate, and not break out-of-tree implementers. Trade-off discussed inline. Also prefer XML docs over // for member contracts. |
| 2 | Low | SnapSyncRunner.cs:22 |
Load-before-dispatch is now a temporal invariant rather than a structural one. Not currently reachable as a bug; a once-guarded lazy load inside ProgressTracker would restore the guarantee and avoid the new public LoadProgress() + ctor dependency. |
| 3 | Low | SnapProvider.cs:325 |
Extra MemDb allocated per refreshed-account response for a slot that is never read or written; a static readonly instance suffices. |
| 4 | Low | ProgressTracker.cs:188 |
The flag is re-recorded (Put + Flush) on every idle IsFinished(), including right after a restart that just read it back. Pre-existing shape, cheap to make write-once now. |
One question on the upgrade path
The fix stops the new writes, but nothing removes an AccountProgressKey that a pre-fix flat run already left in the patricia state DB. Such a node still trips FlatStateActivationPolicy.DecideBackend's patriciaStateDb.Value.GetAllKeys().Any() check on its next restart and flips to patricia once more. For a node that already flipped and started writing patricia trie nodes there's nothing to recover, so this may well be intended — worth a line in the PR description either way, or a Remove in the policy if affected nodes are meant to heal on upgrade.
Tests
Coverage is good and well targeted: the drain→mark path, the skip and no-skip branches of LoadProgress, cross-instance read-back for patricia, and non-persistence for flat. Pinning the on-disk key as a literal in PatriciaSnapTrieFactoryTests with a comment explaining why it's duplicated is exactly right for a format-compatibility test. SnapSyncRunnerTests asserting order through the factory stub is a bit indirect (IsRangePhaseFinished recorded as "LoadProgress"), but it does pin the ordering that matters.
One note: FlatSnapTrieFactoryTests.RangePhase_NeverCarriesIntoTheNextRun asserts a constant, so it becomes a test of the interface default if you take finding #1.
Caveat: builds and dotnet test are blocked in this environment (no network/build permission), so the above is static analysis — I did not compile or run the suite. Everything I could check by reading (call sites, DI ordering, keyed-DB availability in every container that resolves these types) is consistent.
• branch fix/snap-progress-per-backend
|
Claude finished @batrr's task in 4m 29s —— View job Review:
|
| # | Where | Status |
|---|---|---|
| 1 | ISnapTrieFactory defaults |
Closed — your argument is the right one. The existing EnsureInitialize() { } / FinalizeSync() { } defaults describe a backend that keeps its store, so defaulting IsRangePhaseFinished() to false would tell exactly that kind of backend it has no progress. Forcing each backend to state its answer is correct here. |
| 2 | SnapSyncRunner load ordering |
Closed. Keeping the call next to EnsureInitialize() in the one place that owns the run lifecycle is a fair trade, and the failure mode really is re-requesting fetched ranges, not skipping data. Re-verified on the merged tree: SimpleDispatcher<SnapSyncBatch> is constructed only in Synchronizer.cs:451 and consumed only by SnapSyncRunner, and ISnapProvider.IsSnapGetRangesFinished()/CanSync() have no production caller outside the tracker/provider pair — so nothing can observe the tracker before LoadProgress(). |
| 3 | SnapProvider.cs:325 allocation |
Fixed, and NullDb.Instance is the better call than my suggestion. Verified: NullDb.Get returns null (no throw), so IsRangePhaseFinished() would be safely false; Set throws but is unreachable — SnapProviderHelper.VerifyAccountRange only calls CreateStateTree(), and grep confirms the two new members are called from ProgressTracker alone. Your point about the catch (Exception) in VerifyRefreshedAccount swallowing it either way is also correct. |
| 4 | ProgressTracker.cs:188 repeated write |
Acceptable as-is on closer reading. MarkRangePhaseFinished() is only reached when IsSnapGetRangesFinished() is true — which requires all four active-request counters to be zero — and that same call then returns true and ends the dispatcher. So it's ~one Put+Flush per run, not per idle poll. Withdrawing the suggestion. |
What I verified this pass
- The diagnosis is complete for the reported symptom. I checked what else could leave a key in the patricia state DB during a flat snap sync + heal:
ITreeSyncStoreresolves toFlatTreeSyncStoreon a flat node (soPatriciaTreeSyncStorenever writes), andStateBoundaryStoreonly everRemoves from the state DB. The progress key really does look like the sole writer that tripsFlatStateActivationPolicy'spatriciaStateDb.Value.GetAllKeys().Any(). - Format compatibility is preserved. Same key bytes (
"AccountProgressKey"u8), same value (MaxValue), sameDisableWAL+Flush, same read predicate (Length: 32 && == MaxValue). The droppedelsebranch was dead — onlyMaxValue/absent were ever stored. No references to the removedProgressTracker.ACC_PROGRESS_KEYremain. - No eager DB touch on flat nodes.
WorldStateDbDeciderModule.cs:53resolves the patricia factory throughFunc<PatriciaSnapTrieFactory>, so the new[KeyFilter(DbNames.State)] IDbparameter is never resolved when flat wins — the added dependency can't open the patricia DB on a flat node. - Every construction site has a keyed State
IDb. All five sites checked:Synchronizer.cs:444andPruningTrieStoreModule.cs:107(prod,DbModulepresent),StateSyncFeedTestsBase.cs:137(registers it explicitly),TestSynchronizerModule(loadsDbModule), and the directnewinSnapProvider.WithAttributeFiltering()is applied by the container DSL, which is the same mechanismProgressTracker's old[KeyFilter(DbNames.State)]relied on. LoadProgress()is idempotent across repeated runs. It early-returns unless the flag is set, and the clearing branch only runs when phase 1 genuinely completed, so a secondSnapSyncRunner.Run(or a mid-phase-2 restart) can't discard legitimately re-queued partitions. It's also unreachable whenSync.SnapSync=false, which is fine — nothing queries the tracker on that path.
Remaining notes (both optional)
Low — upgrade path for already-affected nodes. Still worth a line in the PR description. A node that hit this bug on a pre-fix build has an AccountProgressKey sitting in its patricia state DB; on the fixed build DecideBackend still sees GetAllKeys().Any() == true and picks patricia. If it already flipped once and wrote patricia trie nodes there's nothing to recover and staying patricia is the right answer — but if it stopped before that first flip, this PR alone doesn't heal it. Either state that explicitly, or Remove the key in the policy. Scope call; I'd lean toward just documenting it.
Low — trivia at ProgressTracker.cs:464: _logger.Info($"Snap - State Ranges (Phase 1) is finished.") has no interpolation holes, so the $ is redundant. Carried over verbatim from the old GetSyncProgress, so leaving it is also defensible.
Tests
Good coverage, well targeted: drain→mark, both branches of LoadProgress, cross-instance read-back for patricia, non-persistence for flat. Pinning the on-disk key as a literal in PatriciaSnapTrieFactoryTests with a comment explaining why it's duplicated is exactly the right instinct for a format-compatibility test. FlatSnapTrieFactoryTests.RangePhase_NeverCarriesIntoTheNextRun is close to a tautology, but since you kept the interface members abstract it does pin the flat contract, so it earns its place.
Caveat: dotnet build / dotnet test are not permitted in this environment, so the above is static analysis — I did not compile or run the suite. Everything checkable by reading (call sites, DI ordering, keyed-DB availability in every container that resolves these types, NullDb semantics) is consistent.
• branch fix/snap-progress-per-backend
flcl42
left a comment
There was a problem hiding this comment.
Found 2 medium-severity issues in src/Nethermind/Nethermind.State.Flat/Sync/Snap/FlatSnapTrieFactory.cs and src/Nethermind/Nethermind.Synchronization/SnapSync/ISnapTrieFactory.cs.
…FlatSnapTrieFactory
…Eth/nethermind into fix/snap-progress-per-backend
|
@claude re review |
|
Claude finished @batrr's task in 8m 47s —— View job Re-review:
|
| # | Where | Status |
|---|---|---|
| 1 | ISnapTrieFactory defaults |
Fixed (defaults added; reasoning above). |
| 2 | SnapSyncRunner load ordering |
Closed, re-verified on this tree. ISnapProvider has exactly one production consumer (SnapSyncFeed), which touches IsFinished only from PrepareRequest — dispatcher-driven. ISnapSyncRunner.Run has one call site (StateSyncRunner.cs:52), once per process. Nothing can observe the tracker before LoadProgress(). |
| 3 | SnapProvider.cs:325 allocation |
Fixed with NullDb.Instance. Verified NullDb.Get returns null rather than throwing (NullDb.cs:24), so IsRangePhaseFinished() is safely false; Set throws but is unreachable. |
| 4 | ProgressTracker.cs:188 repeated write |
Withdrawn. IsSnapGetRangesFinished() requires all four active-request counters at zero, and returning true ends the dispatcher — ~one Put+Flush per run. |
| 5 | Upgrade path (flcl42 + mine) | Acknowledged with rationale; correct, and I can now confirm no regression — see below. |
On the upgrade path, precisely
You said such a node "still works correctly, just on the wrong backend." That's right, and here is the exact mechanism, which is worth having on the record: the stale AccountProgressKey makes DecideBackend pick patricia, and then PatriciaSnapTrieFactory.IsRangePhaseFinished() reads true against a patricia state DB that holds only that key and no trie nodes — so LoadProgress() clears every account-range partition and snap phase 1 is skipped entirely. The node then downloads the whole trie through the phase-2 state-sync feed. Slow, but correct.
The part that settles it: this is byte-for-byte the pre-PR behaviour for that node — the old ProgressTracker read the same key from the same DB in its constructor and skipped phase 1 the same way. So the PR neither causes nor worsens it; it only fails to heal a pre-existing on-disk state. No objection to leaving it.
What I verified this pass
- No new eager resolution or DI cycle.
ProgressTracker's newISnapTrieFactorydependency has only two consumers (SnapProvider,SnapSyncRunner), both of which already depended onISnapTrieFactory.WorldStateDbDeciderModule.cs:53resolves patricia throughFunc<PatriciaSnapTrieFactory>, so the new[KeyFilter(DbNames.State)] IDbparameter is never resolved on a flat node — the added dependency cannot open the patricia DB there. - Keyed-DB availability at every site.
WithAttributeFiltering()is applied byCommonNethermindConfig()(ContainerBuilderExtensions.cs:490-493), which theAddSingletonDSL routes through, so[KeyFilter]resolves forPatriciaSnapTrieFactorywherever it's registered. All five construction sites checked:Synchronizer.cs:444,PruningTrieStoreModule.cs:107,WorldStateDbDeciderModule.cs:53,StateSyncFeedTestsBase.cs:140(registersAddKeyedSingleton<IDb>(DbNames.State, …)at line 137), and the directnewinSnapProvider. - Format compatibility. Same key bytes, same value, same
DisableWAL+Flush, same read predicate, and the same[KeyFilter(DbNames.State)] IDbthe old tracker used — so existing patricia DBs read back unchanged. The droppedelsebranch was dead (onlyMaxValue/absent were ever stored). No reference to the removedProgressTracker.ACC_PROGRESS_KEYsurvives. SnapSyncRunner's two constructors still bind unambiguously. Both now take three parameters, butFunc<CancellationToken, Task>is not a registered service, so only theSimpleDispatcheroverload is bindable — the same tie that already existed at two parameters.- Usings and API surface.
Nethermind.Coreis still needed inProgressTracker(BlockHeader),System.Linqstill used (line 550). New test file:BytesToArray()is an instance method onHash256(Nethermind.Core.Crypto),NodeStorageis inNethermind.Trie,Assert.EnterMultipleScope()is used elsewhere in the repo — all imports present.
Remaining Low notes
FlatSnapTrieFactoryTests.cs:73— posted inline. The test now exercises only the interface default; still useful as a guard, but the intent isn't visible without a comment.ISnapTrieFactory.cs:18-20— the contract for these members is in//comments; repo rules prefer XML<summary>/<remarks>for member-level docs. The content is good (it already spells out the non-persisting default), just the wrong tag.ProgressTracker.cs:464—_logger.Info($"Snap - State Ranges (Phase 1) is finished.")has no interpolation holes, so the$is redundant. Carried over verbatim from the oldGetSyncProgress, so leaving it is defensible.
Tests
Coverage is right on target: drain→mark, both branches of LoadProgress, cross-instance read-back for patricia, non-persistence for flat, and ordering through SnapSyncRunner. Pinning the on-disk key as a literal in PatriciaSnapTrieFactoryTests with a comment explaining why it's duplicated is exactly the right instinct for a format-compatibility test.
Caveat: dotnet build / dotnet test are not permitted in this environment, so this is static analysis — I did not compile or run the suite. Everything checkable by reading (call sites, DI ordering, keyed-DB availability, NullDb semantics, namespace/import resolution for the new test file) is consistent.
• branch fix/snap-progress-per-backend
Changes
If a flat node stops during healing after snap sync, after restart it comes back as a patricia node.
Snap sync stores its range progress in the state DB, whatever the backend is:
and the same DB is what decides whether flat is active:
The fix stores the progress per backend: patricia keeps it in the state DB as before, flat stores nothing because it wipes its DB on every run.
Types of changes
What types of changes does your code introduce?