Summary
When a leader-driven snapshot install "gives up" on a database that has failed to refresh 3 times in a row, the install still ACKs the snapshot index, writes it as applied for every database, clears the stale-read floor and the diverged marks, and returns the installed TermIndex to Ratis (which then purges the log). The node re-enters the ready set advertising fully-caught-up while one database is still stale — so LINEARIZABLE / read-your-writes reads of that database can be served from stale state.
Where (HEAD af4f67fd2)
ha-raft/src/main/java/com/arcadedb/server/ha/raft/DatabaseReconciler.java:
:99 ACQUIRE_GIVE_UP_AFTER = 3; :313-341 bumpFailureAndShouldRetry returns false once a database has failed 3 consecutive times, so applyReconcileOutcome returns false.
:270-272 reconcile only fails the install while that returns true:
if (applyReconcileOutcome(plan, outcome))
throw new IOException("Reconcile from leader had database failure(s); will retry ...");
Past the give-up threshold it returns normally, and ArcadeStateMachine.installSnapshotFromLeader (:1355-1384) unconditionally proceeds:
registerSnapshotMarker(snapshotTerm, snapshotIndex);
lastAppliedIndex.set(snapshotIndex);
writePersistedAppliedIndexForAllDatabases(snapshotIndex); // :1365 — includes the STALE database
clearStaleSnapshotFloor(); // :1369
... raftHA.notifyApplied();
clearDivergedState(); // :1380
return installedTermIndex; // Ratis advances applied idx and purges the log
The read gate is global, not per-database: RaftHAServer.getTrustedAppliedIndex (:2834-2838) returns min(getLastAppliedIndex(), floor); with the floor cleared it returns snapshotIndex for reads targeting any database, so waitForAppliedIndex (:2705) / ensureLinearizableFollowerRead (:2967) pass immediately for readIndex <= snapshotIndex.
Failure sequence (3 nodes, follower F behind on database X)
- Ratis drives InstallSnapshot on F; the refresh of
X fails (disk pressure, CRC/size mismatch, transient fault) and install() rolls X back to its old, behind copy.
- Ratis re-drives;
X fails again. On the 3rd consecutive failure reconcile returns normally instead of throwing.
- The install records
snapshotIndex as applied for all databases incl. X, clears the floor and X's diverged mark, and returns to Ratis, which purges the log up to snapshotIndex — the entries needed to advance X are gone from F.
- F advertises applied =
snapshotIndex. A LINEARIZABLE (or RYW with bookmark ≤ snapshotIndex) read of X routed to F passes the wait instantly and is served from the stale copy.
- Nothing re-arms it:
checkStaleFollower sees zero lag, the floor was cleared, checkStuckFollower is term-based. Recovery waits for the next leader change or the next write to X (which trips WALVersionGapException → re-diverge). On an idle X the stale-read window persists.
Suggested fix
Do not treat a given-up per-database failure as a clean install. Any of: (a) keep the affected database in divergedDatabases (clear only databases that actually refreshed) and consult a per-database freshness gate on the read path; (b) refuse to return the installed TermIndex when any planned database did not reach snapshotIndex, so Ratis retries / the node stays NOT_READY; or (c) at minimum keep a per-database "not-at-snapshotIndex" floor so getTrustedAppliedIndex clamps below the missing entries for reads targeting that database (mirroring the #6111 floor, but per-database).
Scope / confidence
Medium-high that the path exists as described (every line verified at HEAD); the stale-read consequence is traced through the read-gate wiring. Not executed — HA is not runnable here. Requires arcadedb.ha.autoAcquireDatabases=true (the default) plus one database whose snapshot install fails ≥3 consecutive times while the leader still reports it readable.
Summary
When a leader-driven snapshot install "gives up" on a database that has failed to refresh 3 times in a row, the install still ACKs the snapshot index, writes it as applied for every database, clears the stale-read floor and the diverged marks, and returns the installed
TermIndexto Ratis (which then purges the log). The node re-enters the ready set advertising fully-caught-up while one database is still stale — so LINEARIZABLE / read-your-writes reads of that database can be served from stale state.Where (HEAD
af4f67fd2)ha-raft/src/main/java/com/arcadedb/server/ha/raft/DatabaseReconciler.java::99ACQUIRE_GIVE_UP_AFTER = 3;:313-341bumpFailureAndShouldRetryreturnsfalseonce a database has failed 3 consecutive times, soapplyReconcileOutcomereturnsfalse.:270-272reconcile only fails the install while that returnstrue:Past the give-up threshold it returns normally, and
ArcadeStateMachine.installSnapshotFromLeader(:1355-1384) unconditionally proceeds:The read gate is global, not per-database:
RaftHAServer.getTrustedAppliedIndex(:2834-2838) returnsmin(getLastAppliedIndex(), floor); with the floor cleared it returnssnapshotIndexfor reads targeting any database, sowaitForAppliedIndex(:2705) /ensureLinearizableFollowerRead(:2967) pass immediately forreadIndex <= snapshotIndex.Failure sequence (3 nodes, follower F behind on database
X)Xfails (disk pressure, CRC/size mismatch, transient fault) andinstall()rollsXback to its old, behind copy.Xfails again. On the 3rd consecutive failure reconcile returns normally instead of throwing.snapshotIndexas applied for all databases incl.X, clears the floor andX's diverged mark, and returns to Ratis, which purges the log up tosnapshotIndex— the entries needed to advanceXare gone from F.snapshotIndex. A LINEARIZABLE (or RYW with bookmark ≤snapshotIndex) read ofXrouted to F passes the wait instantly and is served from the stale copy.checkStaleFollowersees zero lag, the floor was cleared,checkStuckFolloweris term-based. Recovery waits for the next leader change or the next write toX(which tripsWALVersionGapException→ re-diverge). On an idleXthe stale-read window persists.Suggested fix
Do not treat a given-up per-database failure as a clean install. Any of: (a) keep the affected database in
divergedDatabases(clear only databases that actually refreshed) and consult a per-database freshness gate on the read path; (b) refuse to return the installedTermIndexwhen any planned database did not reachsnapshotIndex, so Ratis retries / the node stays NOT_READY; or (c) at minimum keep a per-database "not-at-snapshotIndex" floor sogetTrustedAppliedIndexclamps below the missing entries for reads targeting that database (mirroring the #6111 floor, but per-database).Scope / confidence
Medium-high that the path exists as described (every line verified at HEAD); the stale-read consequence is traced through the read-gate wiring. Not executed — HA is not runnable here. Requires
arcadedb.ha.autoAcquireDatabases=true(the default) plus one database whose snapshot install fails ≥3 consecutive times while the leader still reports it readable.