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.
| public void FinalizeSync() => persistence.Flush(); | ||
|
|
||
| public bool IsRangePhaseFinished() => false; | ||
| public void MarkRangePhaseFinished() { } |
There was a problem hiding this comment.
[MEDIUM] Legacy flat progress markers still select the Patricia backend
Making the flat marker write a no-op only prevents markers created by this version. If a flat node completed the range phase on an older build and stopped during healing while its flat state is still PreGenesis, its state DB already contains AccountProgressKey; FlatStateActivationPolicy runs before this factory and treats any key there as Patricia state. The upgraded node therefore selects Patricia and never reaches this no-op, reproducing the reported restart failure for existing on-disk state. Compatibility handling for the legacy marker would cover this upgrade path.
|
|
||
| // Marked when the range phase drains, read after EnsureInitialize, so a later run over the same data | ||
| // skips the phase. Only a backend that keeps its store across runs can report true. | ||
| bool IsRangePhaseFinished(); |
There was a problem hiding this comment.
[MEDIUM] Required progress members break existing backend implementations
ISnapTrieFactory is public, and the two new members have no default bodies even though its existing lifecycle members do. An out-of-tree state backend compiled against the previous contract lacks these methods: rebuilding fails, while an existing binary cannot service the LoadProgress and completion calls when snap sync starts. Default false and no-op semantics would preserve prior behavior for implementations that do not persist progress.
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?