Repository navigation
[#1227] Keep the record a changelog cursor read in advance when the head log file it is positioned in is rotated - #1228
Merged
vharseko merged 1 commit intoOct 21, 2026
Conversation
…n advance when the head log file it is positioned in is rotated Fixes OpenIdentityPlatform#1227 ## Problem A replication server can skip one change of a replica for a directory server which is catching up, and the state of that directory server then moves past the change for good. This is the one-change loss of OpenIdentityPlatform#1225, which `docker-convergence (combined)` showed again on a build with OpenIdentityPlatform#1219. A cursor on a changelog log reads the record it is positioned on in advance: `LogFileCursor.positionTo()` keeps it in `initialRecord` for the next `next()`, and leaves the file pointer after it. When the head log file is rotated, `Log.rotateHeadLogFile()` moves every cursor open on it to the read-only file it was renamed to, from the state `InternalLogCursor.getState()` saved: the file position and the current record, but not `initialRecord`. A cursor positioned on the head log file and moved before it read anything therefore starts after the record it is positioned on, and `FileReplicaDBCursor` takes the next one for the first change after its start CSN. `positionTo()` and the first `next()` take the shared lock of the log one after the other, so a rotation which asks for the exclusive lock in between runs before the read. `MessageHandler.fillLateQueue()` makes such cursors anew for every batch it reads, and `FileReplicaDBCursor.nextWhenCursorIsExhaustedOrNotCorrectlyPositionned()` positions again each time it looks for the changes written since. ## Fix - `Log.CursorState` keeps the record read in advance, and the state moved to the rotated file carries it. - `LogFile.LogFileCursor.getInitialRecord()` gives it to the state, and `LogFile.getCursorInitialisedTo()` gives it back to the cursor made on the rotated file. ## Tests `LogTest`, on its log which rotates after two records: - `aCursorReadsTheRecordItIsPositionedOnWhenTheHeadLogFileIsRotatedBeforeItsFirstRead`: a cursor positioned on the head log file, for every key matching and positioning strategy which reads a record in advance, reads that record first after a rotation. - `aCursorPositionedAgainReadsTheRecordItIsPositionedOnWhenTheHeadLogFileIsRotatedBeforeItsNextRead`: the same for a cursor which read the newest record and is positioned again once more were written.
maximthomas
approved these changes
Oct 21, 2026
maximthomas
left a comment
Contributor
There was a problem hiding this comment.
praise: the fix carries the record read in advance along the only road the cursor state travels, and the new tests are a real pin.
CursorStateis produced only byInternalLogCursor.getState(Log.java:1305, called from the rotation at:1084) and consumed only byupdateOpenedCursorsOnHeadAfterRotation(:1051) →reinitializeTo(:1325); all three now carryinitialRecord, and the invalid-state constructor clears it too (:1626).- With
Log.javaandLogFile.javaat the base andLogTestat the head, all 7 new rows fail on the record after the expected one (key009→key010,key010→key011,key012→key013) and the other 88 pass; at the headLogTestis 95/95 on CI (ubuntu-latest, JDK 27).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1227
Problem
A replication server can skip one change of a replica for a directory server which is catching up, and the state of that directory server then moves past the change for good. This is the one-change loss of #1225, which
docker-convergence (combined)showed again in run 39407529872 on a build with #1219. #1227 has the analysis of that run.A cursor on a changelog log reads the record it is positioned on in advance:
LogFileCursor.positionTo()keeps that record ininitialRecordfor the nextnext(), and leaves the file pointer after it.Log.rotateHeadLogFile()moves every cursor open on it to the read-only file it was renamed to. It does so from the stateInternalLogCursor.getState()saved: the file position and the current record, but notinitialRecord.FileReplicaDBCursortakes the next one for the first change after its start CSN.positionTo()and the firstnext()take the shared lock of the log one after the other. So a rotation which asks for the exclusive lock in between runs before the read. Two roads go through that window:MessageHandler.fillLateQueue()makes such cursors anew for every batch it reads.FileReplicaDBCursor.nextWhenCursorIsExhaustedOrNotCorrectlyPositionned()positions again each time it looks for the changes written since.Fix
Log.CursorStatekeeps the record read in advance, and the state moved to the rotated file carries it.LogFile.LogFileCursor.getInitialRecord()gives that record to the state.LogFile.getCursorInitialisedTo()gives it back to the cursor made on the rotated file.Tests
In
LogTest, on its log which rotates after two records:aCursorReadsTheRecordItIsPositionedOnWhenTheHeadLogFileIsRotatedBeforeItsFirstRead: a cursor positioned on the head log file reads that record first after a rotation. It covers every key matching and positioning strategy which reads a record in advance, 6 cases.aCursorPositionedAgainReadsTheRecordItIsPositionedOnWhenTheHeadLogFileIsRotatedBeforeItsNextRead: the same for a cursor which read the newest record and is positioned again once more were written. This is the roadFileReplicaDBCursortakes.Verification
Without the fix: 7 of the 95 tests of
LogTestfail, each one on the skipped record (expectedkey009,key010orkey012, read the record after it).With the fix: 95 of 95 pass.
Mutants: each link of the fix was broken in turn, 5 mutants:
Each one fails all 7 new cases.
Reactor (
-Pprecommit verify -pl opendj-server-legacy -am, JDK 27, with javadoc),org/opends/server/replication/**andChangelogBackendTestCase:setUpofFileReplicaDBTestandComputeBestServerTest, on0.0.0.0:65534: Address already in use: another test server held the admin port.replication/server/changelog/filepasses, andChangelogBackendTestCasepasses (31).