[LocalDb] FailClosedOnDrift is skipped during snapshot sync — a future-stamped row jumps the clock #39
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the scadaproj#15 shared-library review (
code-reviews/LocalDb/findings.md, LocalDb-004), verified at91fb2b4. Update the finding's Resolution row when fixed.ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb.Replication/Internal/SnapshotStreamer.cs:249Description
The drift guard (
MaxHlcDriftAhead+FailClosedOnDrift) lives only inSyncSession.ApplyDeltaEntriesAsync(SyncSession.cs:599-628).SnapshotApplier.OnBatchAsyncpasses every snapshot row straight to
LwwApplier.ApplyBatchAsync. It takes noReplicationOptions, so it cannot check drift. The applier then callsdb.Clock.Observe(maxHlc)(LwwApplier.cs:155), andOnCompleteAsyncpersistslast_seen_hlc.That value feeds clock recovery on every later open (
SqliteLocalDb.cs:69-75).This contradicts README §Semantics ("With
FailClosedOnDrift, each over-drift entry isdead-lettered") and design §Error handling ("configurable max-drift guard (warn, optionally
fail-closed)").
Failure scenario
Reproduced in a scratch test with
FailClosedOnDrift = true. A peer whose clock was a year aheadwhen it wrote a row serves a snapshot after a cap prune or rebuild. The receiving node writes 0
dead-letters, applies the row, and its HLC jumps about 365 days ahead. Every later local write on
that node is stamped a year in the future, and the stamp survives restarts through
last_seen_hlc.From then on those writes beat every genuine write on the peer under LWW until wall-clock time
catches up. The operator enabled
FailClosedOnDriftspecifically to prevent this.Recommendation
Give
SnapshotApplierthe options and the clock, and run the same per-row drift check as the deltapath: dead-letter over-drift rows when
FailClosedOnDrift, otherwise warn once. Better, move thecheck into
LwwApplierso both paths share it. Add a snapshot-drift test.Resolution
Unresolved.
Fixed on main
c207573(LocalDb-004 → Resolved): 2-node test reproduced a year-ahead row applied with no dead-letter. The snapshot path now runs the same drift check as the delta path, and under FailClosedOnDrift a rejected row's timestamp is no longer saved as last_seen_hlc. Fail-closed test fails without the fix; warn-only still applies. Evidence: LocalDb 236/236; Auth 344 passed / 78 skipped (live SQL/LDAP gated) after rebase; code-reviews --check consistent.