Repository navigation
[#1195] Publish every change done while a recovery runs, and end the recovery only once they are out - #1204
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The three losses are fixed where they happen, and the fix for the hand-over is atomic.
PendingChanges.recoveryUntil()runsonRecoveredunder the lockpushCommittedChanges()takes, so no change can be pushed between the end of the recovery and the broker taking changes again (PendingChanges.java:291-299).- On a refusal the round puts the refused
FakeOperationand every one after it back intoreplayOperations(LDAPReplicationDomain.java:6993-7008), so a delete kept aside is not dropped. - The window end moves from seqnum
0xfffffffftoInteger.MAX_VALUE, which orders the same as aCSNand asds-sync-histindex text, so the last millisecond of a window belongs to it.
issue (blocking): A refused recovery publish leaves PendingChanges recovering, and a next session that starts no recovery publishes no local change.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:6993-7008, :6868-6875, :862-868, :6788-6803; opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChanges.java:197-205, :291-299
When publishRecovery() returns false (connectionError, or a broker stopped by disable()/restartService()), the round returns MissingChangesRound(fromCSN, false, false, false), buildAndPublishMissingChanges() returns false before recoveryUntil() runs, and the RSUpdater finally clears only the broker flag. recoveringOldChanges is cleared only by recoveryUntil(); setRecovering() has a single caller, which passes true. The next sessionInitiated() starts no recovery when the RS's CSN for this replica is at or past ours (no local change since the refusal, e.g. back on the RS that had everything before the switch), when the RS holds none or seqnum 0, or when the rsUpdater compareAndSet loses to the old thread (#1199). For that whole session pushCommittedChanges() only records each local change in the ServerState and publishes none of them. On master the refusal was ignored, recoveryUntil() ended the recovery and that session published normally. The description's "left to the recovery of the next session" assumes a recovery that these branches never start. If routing a refusal into the #1197 exit was deliberate, it also needs the pending changes to leave recovery when no recovery follows.
// PendingChanges
/** Ends a recovery which failed and which no recovery of a later session ends. */
synchronized void endRecoveryLeftByAFailedOne(Runnable onRecovered)
{
if (recoveringOldChanges)
{
recoveringOldChanges = false;
onRecovered.run();
}
}
// LDAPReplicationDomain.sessionInitiated()
boolean recoveryStarted = false;
if (replServerMaxCSN != null && replServerMaxCSN.getSeqnum() != 0)
{
CSN ourMaxCSN = state.getMaxCSN(getServerId());
if (ourMaxCSN != null
&& !ourMaxCSN.isOlderThanOrEqualTo(replServerMaxCSN))
{
pendingChanges.setRecovering(true);
broker.setRecoveryRequired(true);
final RSUpdater rsUpdater = new RSUpdater(replServerMaxCSN);
recoveryStarted = this.rsUpdater.compareAndSet(null, rsUpdater);
if (recoveryStarted)
{
rsUpdater.start();
}
}
}
if (!recoveryStarted && this.rsUpdater.get() == null)
{
pendingChanges.endRecoveryLeftByAFailedOne(() -> broker.setRecoveryRequired(false));
}Pin: after a recovery the broker refused (and the broker flag cleared as the RSUpdater finally does), start a session against an RS that holds the domain's last CSN, then replace(dnA, …) must pass assertInTheChangelog. The #1199 race stays, as filed.
suggestion (non-blocking): No test pins that the broker flag is cleared under the PendingChanges lock.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:7024-7025, opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/HistoricalCsnOrderingTest.java:455
aChangeDoneOnceTheRecoveryEndedIsPublished fails if onRecovered.run() is deleted, but not if it moves: pass () -> {} and call broker.setRecoveryRequired(false) after if (round.endedRecovery), outside every monitor, and the class stays green. The single-thread case changes only after the call returns, and the two writer cases end on a held round, where the moved clear still runs inside holdBackLocalChangesWhile()'s monitor. The hand-over race of the third bullet could come back unnoticed.
// in a copy of aChangeDoneOnceTheRecoveryEndedIsPublished, after startRecovery(rd1)
// (import java.util.concurrent.atomic.AtomicBoolean)
final Field pendingChangesField = LDAPReplicationDomain.class.getDeclaredField("pendingChanges");
pendingChangesField.setAccessible(true);
final Object pendingChanges = pendingChangesField.get(rd1);
final Field brokerField = ReplicationDomain.class.getDeclaredField("broker");
brokerField.setAccessible(true);
final ReplicationBroker realBroker = (ReplicationBroker) brokerField.get(rd1);
final AtomicBoolean clearedUnderTheLock = new AtomicBoolean();
brokerField.set(rd1, new TestBroker(new ArrayList<ReplicationMsg>())
{
@Override
public void setRecoveryRequired(boolean required)
{
clearedUnderTheLock.set(!required && Thread.holdsLock(pendingChanges));
}
});
try
{
assertTrue(rd1.buildAndPublishMissingChanges(rsMaxCSN, new TestBroker(new ArrayList<ReplicationMsg>())));
assertTrue(clearedUnderTheLock.get(), "the broker takes changes again under the lock they are pushed under");
}
finally
{
brokerField.set(rd1, realBroker);
realBroker.setRecoveryRequired(false);
}Pin: the case above turns red on the moved clear, because the round that ends the recovery here is not a held one.
suggestion (non-blocking): The refusal road is tested only against the TestBroker stub; the real publishRecovery() result never runs.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java:2283-2286, opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/HistoricalCsnOrderingTest.java:117
Every case passes a TestBroker whose publishRecovery() returns its writes flag, so return publish(msg, true, true) is never executed: the mutant publish(msg, true, true); return true; (master's dropped result) survives the class. changesTheBrokerRefusedAreLeftToTheNextRecovery pins the domain-side put-back only.
@Test
public void aStoppedBrokerRefusesARecoveryMessage() throws Exception
{
final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
TestCaseUtils.initializeTestBackend(true);
ReplicationServer rs = createReplicationServer();
ReplicationBroker broker = null;
try
{
broker = openReplicationSession(baseDN, OTHER_SERVER_ID, 100, rsPort, 1000);
broker.stop();
assertFalse(broker.publishRecovery(new WindowProbeMsg()), "a stopped broker writes nothing");
}
finally
{
stop(broker);
remove(rs);
}
}Pin: publish()'s loop is skipped once shutdown is set, so the case is red on the mutant and green at the head.
issue (non-blocking): stop()/disable() waits on the PendingChanges monitor for every held-back round, without a bound while the RS withholds window credit.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:6868-6871, opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java:2737-2741
holdBackLocalChangesWhile() holds the monitor across the round's search and every publishRecovery(). ReplicationBroker.stop() calls the synchronized PendingChanges.putReplicaOfflineMsg() before shutdown = true, and publish()'s loop ends only on done, connectionError or that flag. A disable() during a held round therefore waits, holding serviceStateLock and sessionLock, and local writes wait with it. Filed as #1202, recorded here only so the follow-up keeps the disable() road in view.
issue (non-blocking): aChangeInTheLastMillisecondOfASearchWindowIsPublished fails on correct code when more than ~10 s pass between addPerson("b") and replace().
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/HistoricalCsnOrderingTest.java:589-604
endOfFirstWindow is fixed right after addPerson("b"). Then the replication session opens (1 s timeout) and waitUntilReplayed() runs (10 s budget) before replace(). CSNGenerator takes the wall clock once it passes the adjusted lastTime, so on a stalled runner the change is numbered past the window and the assertEquals fails.
other = openReplicationSession(baseDN, OTHER_SERVER_ID, 100, rsPort, 1000);
final DN dnA = addPerson("a");
final DN dnB = addPerson("b");
final CSN rsMaxCSN = rd1.getLastLocalChange();
final long endOfFirstWindow = rsMaxCSN.getTime() + SEARCH_WINDOW_MS;suggestion (non-blocking): publishRecovery() changes its descriptor from ()V to ()Z.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java:2283
Code compiled against an earlier release that calls it gets NoSuchMethodError. In this repository the only callers are LDAPReplicationDomain:6993 and the test override. If ReplicationBroker is not meant as an extension point, nothing needs to change; otherwise a line in the release notes covers it.
nitpick (non-blocking): The publishRecovery() @return and the buildAndPublishMissingChanges() javadoc promise more than publish() returns.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java:2279-2281
publish() calls the void Session.publish(msg), which drops Session.publish(msg, null)'s false for a closing session or an unencodable message, and returns true. The javadoc covers only connectionError and a stopped broker.
* @return {@code false} if the broker refused the message: no replication server was reachable,
* or the broker was stopped; {@code true} once a session accepted itnitpick (non-blocking): assertEachChangePublishedOnce does not check "once".
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/HistoricalCsnOrderingTest.java:750-768
The helper checks that the recovered CSNs are an ordered prefix of done and that the rest are in the changelog. It never checks that a recovered CSN was not also published by the broker. Rename it assertEachChangePublished, or add that check.
|
Round 2: 827f8b2. The description is updated. issue (blocking): a refused recovery publish leaves
The snippet also keys on So the session starts a publisher instead. When its replication server misses no change of this replica but
#1197 keeps the rest: a failed publisher still leaves the recovery on for the rest of its own session. suggestion: no test pins that the broker flag is cleared under the suggestion: the real issue: issue: suggestion: nitpick: the nitpick: Tests: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-1 blocker is answered where it arises, and two round-1 nits are fixed.
- The
else if (pendingChanges.isRecovering())arm insessionInitiated()(LDAPReplicationDomain.java:6801-6816) starts a publisher when the replication server misses nothing, so a refused publish no longer leaves the next session publishing nothing;aSessionWhichNeedsNoRecoveryEndsTheOneARefusedPublisherLeftdrives it, andHistoricalCsnOrderingTestran green here at 827f8b2 (14/14). aChangeInTheLastMillisecondOfASearchWindowIsPublishedopens its session before the changes (HistoricalCsnOrderingTest.java:640), so only the replay wait sits inside the window, and the precondition has its own message.ReplicationBroker.publishRecovery()'s@returnand thebuildAndPublishMissingChanges()javadoc now say exactly when a change counts as refused.
issue (blocking): A recovery round whose search fails on a session that stays up leaves PendingChanges recovering with no publisher.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:7050-7052, :6898-6900, :831-867
When the domain's base entry is missing, the range search returns NO_SUCH_OBJECT without an exception (EntryContainer.java:1186, LocalBackendSearchOperation.java:149); succeeded is false, so recoveryUntil() never runs, buildAndPublishMissingChanges() returns false and the RSUpdater finally clears only the broker flag. No next session comes, so every local change is only recorded in the ServerState until the connection drops. On master recoveryUntil() ran before the SUCCESS test with currentStartCSN past the last local change, so the recovery ended and only that window's changes were lost, which is what the unchanged comment in RSUpdater.run() ("This server will start accepting again new updates") still says. A failed window that is not the last, and a search that throws, leave the same state (on master too); the change below covers both. If a failed search was meant to leave the recovery to "the next session": a session that stays up starts none.
// PendingChanges
/** Ends a recovery which cannot go on, so that the local changes are published again. */
synchronized void endRecovery(Runnable onRecovered)
{
if (recoveringOldChanges)
{
recoveringOldChanges = false;
onRecovered.run();
}
}
// LDAPReplicationDomain.publishMissingChanges(), where succeeded is computed
final boolean succeeded = op.getResultCode().equals(ResultCode.SUCCESS);
if (!succeeded)
{
// No next session follows a failed search: what it did not find is left to the repair tool.
pendingChanges.endRecovery(() -> broker.setRecoveryRequired(false));
}
// RSUpdater.run(), in the catch of an exception out of the search, after the logger.error
pendingChanges.endRecovery(() -> broker.setRecoveryRequired(false));Left as they are: the refusal (:7036), which the reconnect and the new arm end, and the shutdown exits (:6875, :6891, round == null).
Pin: delete the domain's base entry during a recovery so its round's search returns NO_SUCH_OBJECT, then re-add it and assert the broker receives the add — red at 827f8b2.
issue (blocking): On a replication server that holds no change of this replica, the new arm's publisher drops the kept-aside deletes, so a later recovery never publishes them.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:6815-6816, :6869-6884, :6993-7010, :835-837, :7027-7033
A publisher fails and leaves the pending changes recovering; a local delete c4 done then is kept aside (:1950) and only recorded in the state. The next session reaches an RS that holds none of this replica's changes: the arm starts the publisher at ourMaxCSN = c4, buildAndPublishMissingChanges() trims every replayOperations key at or below it — c4 and any refused delete put back at :7027-7033 — ends the recovery, and run() clears the map. A later session to an RS whose newest change of this replica is older than c4 cannot find the deleted entry and has no kept-aside delete left: c4 is never published and the other replicas keep the entry. On master no publisher starts on the empty RS, the delete stays kept aside and goes out in that later recovery. Adds and modifies are not hit: that recovery's search finds them in ds-sync-hist.
Fix: for the !replServerHoldsChanges start, leave replayOperations as it is — no trim at :6869-6884, no entry at or below the start CSN taken by the selection at :6996-7009 (skip it without removing it), no clear at :835-837 — and end the recovery as now; the put-back comment at :7022-7026 then holds. Skipping only the trim is not enough: the selection has no lower bound and would send those deletes to the empty RS in the first round.
Or: skip only the trim and let that first round publish them — nothing is lost, but up to 10000 deletes the other replicas already have are re-sent, against the "do not update an empty RS" comment at :6786-6791.
Pin: a refused publisher, a local delete, a session to an RS holding none of this replica's changes (it receives nothing older than ourMaxCSN), then a session to an RS that lacks the delete — the delete is published.
suggestion (non-blocking): Nothing pins putting back the first change the broker refuses.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:7029, opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/HistoricalCsnOrderingTest.java:741-760
In changesTheBrokerRefusedAreLeftToTheNextRecovery the first refused change is the modify of dnA, which the second recovery's search finds again; only the delete of dnB needs a put-back, and the loop after :7029 does that. With replayOperations.put(opToSend.getCSN(), opToSend); deleted, the class ran green here: 14/14.
Pin: swap the two changes at :757-760, so the delete of dnB is the first one refused — red under that mutant.
suggestion (non-blocking): The new arm's ourMaxCSN and new CSN(now, 0, id) starts are run by no test.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:6815-6816, opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/HistoricalCsnOrderingTest.java:782-816
aSessionWhichNeedsNoRecoveryEndsTheOneARefusedPublisherLeft asserts b in the changelog first, so only replServerMaxCSN is taken; collapsing the ternary to it stays green and, on an RS holding none of this replica's changes, passes null, which ends in an NPE at :6980 and the recovery left set.
Pin: the second blocking issue's test runs the ourMaxCSN start; a variant whose domain state holds no CSN of this replica runs the third.
suggestion (non-blocking): No test runs a recovery round whose search is not SUCCESS.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:7050-7052
Whatever the first blocking issue settles, nothing turns red when that outcome flips.
Pin: that issue's test.
question (non-blocking): Does #1202 also cover replays waiting on the entry locks of local operations parked in a held round?
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:6896-6897, opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChanges.java:125
A local modify takes the entry write lock (LocalBackendModifyOperation.java:391) and parks in putLocalOperation(); a replay of the same entry gets BUSY after the lock manager's 9 s, ten times, then takes the BUSY give-up branch (LDAPReplicationDomain.java:3346) and goes down the replay-failure road; a parked delete blocks the whole subtree. A held round lasts as long as its publish, unbounded while the RS withholds window credit or the session reconnects. Minor as long as a held round cannot outlast ~90 s otherwise; bounding the held publish fixes both waits.
issue (non-blocking): The new arm's isRecovering() check and the publisher's compareAndSet race the previous publisher's finally.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:6801-6833, :864-868
If the previous publisher ends the recovery between the check and the compareAndSet, a second one starts and republishes changes already out (the changelog drops them, the cookie notification fires again). If it is failing and has not reached its finally, the compareAndSet loses and the recovery stays set for the whole session, which the comment at :6810-6812 says cannot happen. The window is microseconds against the broker's 500 ms reconnect sleep. Correct the comment; to close it, re-run the arm's check from RSUpdater.run()'s finally after its compareAndSet(this, null).
suggestion (non-blocking): A refused publish logs ERR_CANNOT_RECOVER_CHANGES, whose text blames a search of old changes.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:7036, opendj-server-legacy/src/messages/org/opends/messages/replication.properties:65
Every RS loss during a recovery now logs "Error when searching old changes from the database", and the comment adds that the repair tool must re-synchronize, for changes the next session is built to publish; on master the refusal logged nothing. Give the refusal its own message and keep this one for a failed or throwing search.
nitpick (non-blocking): The window end new CSN(time + 10000, Integer.MAX_VALUE, id) is a CSN this replica can generate.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:6980
The seqnum is not reset per millisecond and reaches Integer.MAX_VALUE before it wraps, so a change numbered exactly the window end is outside this window's isOlderThan(endCSN); whether the next window then misses it was not traced. Practically unreachable; a comment would do.
nitpick (non-blocking): replServers is never cleared, so each later replication server and domain of the class is configured with every earlier case's dead port.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/HistoricalCsnOrderingTest.java:94
The refused loopback connects only delay each connect. Clear it in the class's @AfterMethod, or drop the port in remove(rs).
827f8b2 to
f06769e
Compare
|
Round 3: f06769e. The branch is rebased onto master, which has #1205 now; it touches none of these files, and the two earlier commits are unchanged. The description is updated, and the PR now closes #1197 and #1201 as well. issue (blocking): a recovery round whose search fails on a session which stays up leaves
issue (blocking): on a replication server which holds no change of this replica, the new arm's publisher drops the kept-aside deletes. Confirmed. Fixed wider than you asked: for every start, not only the
The end of any recovery had the same hole: the clear dropped deletes which the replication server of a later session could still miss. The map stays bounded by the cap of 10000 (
It is red on 827f8b2, and on each mutant which brings back the trim, brings back the clear, or removes what the selection skips. suggestion: nothing pins putting back the first change the broker refuses. Swapped: the delete of suggestion: the new arm's suggestion: no test runs a recovery round whose search is not question: does #1202 also cover replays waiting on the entry locks of local operations parked in a held round? No: #1202 and #1210 bound issue (non-blocking): the new arm's check and the compare-and-set race the previous publisher's suggestion (non-blocking): a refused publish logs nitpick: the window end is a CSN this replica can generate. Traced: the change was lost. The search filter takes the end, but the listener took only what is nitpick: Tests: |
…e to give credit once the broker is being stopped, and write nothing after a message given up ReplicationBroker.stop() published the replica offline message before it set shutdown, the one flag which ends the wait of a publish for window credit. That message needs credit as an update does, and it is published under the lock of the pending changes, which a local change holds while it is pushed - and while its publish waits for credit. Credit comes only as the replication server takes the updates of the session, while its heartbeats come from a thread of their own and keep the session up: a replication server which took no update held the stop for as long as it took none, and with it ReplicationDomain.disableService(), which calls stop() holding serviceStateLock and sessionLock - the shutdown of the domain, an import, a restore, a session restart. With the window of the session spent, a stop waited for the credit of its own replica offline message even with no local change in flight. stop() now sets a stopping flag, with a deadline, before it publishes that message, and start() clears it. The replication server gets as long to give credit as it gets to accept a connection (the connection timeout of the replication provider, 5 seconds by default): one which is only slow still takes the changes and the replica offline message, as before. A publish which finds no credit once the deadline is past gives its message up - a local change, which the next session republishes from the historical information of its entry, or a delete from the operations the domain keeps in memory, which a restart of the server loses; or the replica offline message, which PendingChanges.putReplicaOfflineMsg() then reports as not sent, its announcement withdrawn - and from then on the broker writes nothing: written after a change given up, the replica offline message, which carries the newest CSN of the replica, would tell the replication server that nothing older is to come. stop() also stops the broker when publishing that message fails, rather than leave a stopping broker running. Not changed: a publish an IOException holds up still retries until the broker is connected again or finds no replication server to connect to, and a publish blocked in the write to the socket itself is not ended by the flag - with the default window of 100000 updates the socket buffers fill before the window closes, and that hang is OpenIdentityPlatform#1209. The recovery publishes given up are not reported to the recovery on master, as those refused once shutdown is set were not; OpenIdentityPlatform#1204 makes publishRecovery() report them. With the window open, a stop still waits for the end of a round OpenIdentityPlatform#1204 holds the local changes back for, as OpenIdentityPlatform#1202 records. ReplicationBrokerStopTest connects a domain to a replication server which keeps the session up and reads all of it, but gives a window of one update and no credit after it unless the test gives some. Disabling the domain once a change spent the window ends in bounded time, the replica offline message not written, and so does disabling it while a second change waits for credit under the lock, the change ending with it (both red before the change). A change given up is followed by nothing, even with credit coming right after it, and a broker started again writes again (red before the change, where the change waits for credit for good, and on a version which gave up after a single wait for credit, where the replica offline message went out after it). Credit given a second into the stop gets that change and then the replica offline message out (green before the change, red on that version). A broker started again after a stop waits for credit past the deadline of that stop, and a stop with the window open writes the replica offline message (green before the change: they pin what the change keeps). A stop whose replica offline message fails still stops the broker (red before the change). Each of eight mutants is caught: stopping or the give-up not cleared by start(), stopping set only after the replica offline message, no deadline, no give-up kept, a stopping broker writing nothing, only the replica offline message given up, and no finally around it.
maximthomas
left a comment
There was a problem hiding this comment.
praise: Both round-2 blockers are fixed where they arise, and the new put-back is pinned.
RSUpdater.run()'s catch now has a failed recovery requestSessionRestart.NOWafter its wait (LDAPReplicationDomain.java:859-899). A failed search on a session that stays up no longer leavesPendingChangesrecovering with no publisher.aRecoveryWhoseSearchFailedHasTheSessionStartedAgainandaRecoveryWhichThrewHasTheSessionStartedAgaindrive both exits.- The
putBackUnpublished()in thecatch (RuntimeException e)around the publish loop (:7107) is pinned: deleting it turnstheDeletesARoundThrewBeforePublishingAreLeftToTheNextRecoveryred (measured here: 1 of 22 cases red).HistoricalCsnOrderingTestruns green at f06769e (22/22).
issue (blocking): A local delete that fails in the backend now stays among the deletes kept aside, and a later recovery publishes it.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:2518-2524, :1974-1983, :7054-7072
handleConflictResolution() puts a FakeDelOperation into replayOperations for every local delete, before the backend runs it (:1974-1983). If the delete then fails (NOT_ALLOWED_ON_NONLEAF, a pre-operation plugin, the sub-backend check), the failure branch of synchronize(PostOperationOperation) removes it from pendingChanges only. This commit removes the trim at the start of a round and the clear after a successful recovery. Without them the entry stays until a round whose window covers it publishes it (:7056-7071, no other check), or until the 10000 cap evicts it.
Road: the delete fails at csn_f. A later recovery against a replication server that holds later changes of this replica starts past csn_f and skips it. Then a session on a lagging replication server, whose max for this replica is before csn_f, publishes DeleteMsg(csn_f). The replicas fed through that server delete an entry that still exists here, and rename its children as conflicts. Before this commit, the first recovery that started past csn_f trimmed it. Read, not run.
else if (!op.isSynchronizationOperation() && curCSN != null)
{
// Remove an unsuccessful non-replication operation from the pending
// changes list.
pendingChanges.remove(curCSN);
pendingChanges.pushCommittedChanges();
// A delete which failed must not stay among the deletes kept aside: a later recovery would
// publish it.
synchronized (replayOperations)
{
replayOperations.remove(curCSN);
}
}Only a local delete puts its own CSN into replayOperations, so the remove does nothing for the other operation types. No lock nests: the pendingChanges calls return before replayOperations is taken. The fix leaves one window open, and it is older than this PR: a round that takes the delete between the put and the failure still publishes it, or puts it back after the remove (:7129-7133). Not checked: whether a cancelled delete skips the post-operation call.
Pin: a HistoricalCsnOrderingTest case that deletes a non-leaf entry (NOT_ALLOWED_ON_NONLEAF), makes a later local change, runs buildAndPublishMissingChanges() from a CSN before the failed delete, and asserts that no DeleteMsg for that DN is published. It is red at this head without the remove.
issue (non-blocking): While a failed recovery waits to start the session again, the broker drops every message that is not a recovery one.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:877-888, opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java:2346
The catch sleeps in waitBeforeSessionRestart() (1-10 s, longer with each consecutive failure). Only after that does the finally call broker.setRecoveryRequired(false), and while the flag is up ReplicationBroker.publish() returns false for any non-recovery message. During the wait these messages are lost:
- SAFE_READ acks for the updates replayed here, so their originators time out;
- the
ReplicaOfflineMsgof adisable(); - the messages of an export from this replica, so the export fails;
- a
ResetGenerationIdMsg.
If the failure persists, the flag is up about 10 s of every 11-12 s. Before this commit the finally cleared the flag right after the failed round. Local updates are not affected: the recovering PendingChanges keeps them. Read, not run.
// The broker drops every message which is not a recovery one while the flag is up.
broker.setRecoveryRequired(false);
waitBeforeSessionRestart(failures, sessionRestartBackoffWakes());The clear in the finally stays, for the exits that do not wait. Not checked: a session that comes during the wait sets the flag again (:6842) and starts no publisher of its own, so from that reconnect to the finally the flag is up again.
issue (non-blocking): A round counts the NO_SUCH_OBJECT from a backend that a failed import left deregistered as a search that went through, so the changes of that window are never published.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:7042-7045
LocalBackendWorkflowElement answers NO_SUCH_OBJECT for a base DN that has no backend, as it does for a missing base entry. Take a total update into this replica that fails after preBackendImport() deregistered the backend, for example the lock refusal at :5999-6006. The domain stays enabled and the recovery keeps running. A window searched in that state publishes none of its changes, and the round moves on. The recovery ends at a later window, and every later session finds the replication server at or past this replica's max, so those changes never leave this replica. Before this commit the round stopped on the non-SUCCESS result, and the next session searched that window again. This needs three rare conditions at once. Read, not run.
final ResultCode resultCode = op.getResultCode();
if (!resultCode.equals(ResultCode.SUCCESS)
&& !(resultCode.equals(ResultCode.NO_SUCH_OBJECT) && isBaseEntryGone()))
{
throw new MissingChangesSearchFailedException(fromCSN, op);
}
/** Whether the backend of the domain is there and holds no base entry: its search then went through. */
private boolean isBaseEntryGone()
{
final LocalBackend<?> backend = getBackend();
try
{
return backend != null && !backend.entryExists(getBaseDN());
}
catch (DirectoryException e)
{
return false;
}
}Still open with this fix: a backend that comes back between the search and the check.
suggestion (non-blocking): No test pins the wait before a failed recovery starts the session again, or the reset of its failure counter.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:853, :877-882
Only aRecoveryWhoseSearchFailedHasTheSessionStartedAgain reaches the catch, on a fresh domain that fails once. Two deletions leave it green (read, not run):
- deleting
waitBeforeSessionRestart(...)only brings the restart about 1 s earlier, well inside the test's 30 s poll; - deleting
consecutiveRecoveryFailures.set(0)changes nothing on a domain that fails only once.
A tight restart loop, or a wait stuck at the 10 s cap, would pass.
Pin: a case that fails the search twice in a row on one domain and then lets it go through. It asserts that the wait grows on the second failure (1000 → 2000 ms, read from the ERR_MISSING_CHANGES_NOT_PUBLISHED log argument or from a package-private getter of consecutiveRecoveryFailures), and that the count is back to 0 after the success.
…e to give credit once the broker is being stopped, and write nothing after a message given up ReplicationBroker.stop() published the replica offline message before it set shutdown, the one flag which ends the wait of a publish for window credit. That message needs credit as an update does, and it is published under the lock of the pending changes, which a local change holds while it is pushed - and while its publish waits for credit. Credit comes only as the replication server takes the updates of the session, while its heartbeats come from a thread of their own and keep the session up: a replication server which took no update held the stop for as long as it took none, and with it ReplicationDomain.disableService(), which calls stop() holding serviceStateLock and sessionLock - the shutdown of the domain, an import, a restore, a session restart. With the window of the session spent, a stop waited for the credit of its own replica offline message even with no local change in flight. stop() now sets a stopping flag, with a deadline, before it publishes that message, and start() clears it. The replication server gets as long to give credit as it gets to accept a connection (the connection timeout of the replication provider, 5 seconds by default): one which is only slow still takes the changes and the replica offline message, as before. A publish which finds no credit once the deadline is past gives its message up - a local change, which the next session republishes from the historical information of its entry, or a delete from the operations the domain keeps in memory, which a restart of the server loses; or the replica offline message, which PendingChanges.putReplicaOfflineMsg() then reports as not sent, its announcement withdrawn - and from then on the broker writes nothing: written after a change given up, the replica offline message, which carries the newest CSN of the replica, would tell the replication server that nothing older is to come. stop() also stops the broker when publishing that message fails, rather than leave a stopping broker running. Not changed: a publish an IOException holds up still retries until the broker is connected again or finds no replication server to connect to, and a publish blocked in the write to the socket itself is not ended by the flag - with the default window of 100000 updates the socket buffers fill before the window closes, and that hang is OpenIdentityPlatform#1209. The recovery publishes given up are not reported to the recovery on master, as those refused once shutdown is set were not; OpenIdentityPlatform#1204 makes publishRecovery() report them. With the window open, a stop still waits for the end of a round OpenIdentityPlatform#1204 holds the local changes back for, as OpenIdentityPlatform#1202 records. ReplicationBrokerStopTest connects a domain to a replication server which keeps the session up and reads all of it, but gives a window of one update and no credit after it unless the test gives some. Disabling the domain once a change spent the window ends in bounded time, the replica offline message not written, and so does disabling it while a second change waits for credit under the lock, the change ending with it (both red before the change). A change given up is followed by nothing, even with credit coming right after it, and a broker started again writes again (red before the change, where the change waits for credit for good, and on a version which gave up after a single wait for credit, where the replica offline message went out after it). Credit given a second into the stop gets that change and then the replica offline message out (green before the change, red on that version). A broker started again after a stop waits for credit past the deadline of that stop, and a stop with the window open writes the replica offline message (green before the change: they pin what the change keeps). A stop whose replica offline message fails still stops the broker (red before the change). Each of eight mutants is caught: stopping or the give-up not cleared by start(), stopping set only after the replica offline message, no deadline, no give-up kept, a stopping broker writing nothing, only the replica offline message given up, and no finally around it.
f06769e to
f7fcf27
Compare
|
Round 4: f7fcf27. The branch is rebased onto master (8fc60b9, whose changes since round 3 touch the workflows and grizzly only), and the three earlier commits are unchanged. Besides your round 3, this round takes in #1199, #1200 and #1203, two issues filed from your round 3, #1220 and #1221, and #1223, found on the way; the description is rewritten for them, and the PR closes all six as well. issue (blocking): a local delete that fails stays among the deletes kept aside, and a later recovery publishes it. Confirmed, and not only for a backend which refuses it:
A cancelled delete goes the same way: issue (non-blocking): while a failed recovery waits, the broker drops every message that is not a recovery one. Confirmed, and not only during the wait: the check at Your snippet is not taken, as it would let nothing more through. The two
issue (non-blocking): NO_SUCH_OBJECT from a backend that a failed import left deregistered. Confirmed. Your suggestion (non-blocking): no test pins the wait before a failed recovery starts the session again, or the reset of its counter. Added
It is red on the mutant without the wait, and on the one without the reset. A session which hands its start over during the wait ends it now: The rest of this round.
Tests: |
…e to give credit once the broker is being stopped, and write nothing after a message given up ReplicationBroker.stop() published the replica offline message before it set shutdown, the one flag which ends the wait of a publish for window credit. That message needs credit as an update does, and it is published under the lock of the pending changes, which a local change holds while it is pushed - and while its publish waits for credit. Credit comes only as the replication server takes the updates of the session, while its heartbeats come from a thread of their own and keep the session up: a replication server which took no update held the stop for as long as it took none, and with it ReplicationDomain.disableService(), which calls stop() holding serviceStateLock and sessionLock - the shutdown of the domain, an import, a restore, a session restart. With the window of the session spent, a stop waited for the credit of its own replica offline message even with no local change in flight. stop() now sets a stopping flag, with a deadline, before it publishes that message, and start() clears it. The replication server gets as long to give credit as it gets to accept a connection (the connection timeout of the replication provider, 5 seconds by default): one which is only slow still takes the changes and the replica offline message, as before. A publish which finds no credit once the deadline is past gives its message up - a local change, which the next session republishes from the historical information of its entry, or a delete from the operations the domain keeps in memory, which a restart of the server loses; or the replica offline message, which PendingChanges.putReplicaOfflineMsg() then reports as not sent, its announcement withdrawn - and from then on the broker writes nothing: written after a change given up, the replica offline message, which carries the newest CSN of the replica, would tell the replication server that nothing older is to come. stop() also stops the broker when publishing that message fails, rather than leave a stopping broker running. Not changed: a publish an IOException holds up still retries until the broker is connected again or finds no replication server to connect to, and a publish blocked in the write to the socket itself is not ended by the flag - with the default window of 100000 updates the socket buffers fill before the window closes, and that hang is OpenIdentityPlatform#1209. The recovery publishes given up are not reported to the recovery on master, as those refused once shutdown is set were not; OpenIdentityPlatform#1204 makes publishRecovery() report them. With the window open, a stop still waits for the end of a round OpenIdentityPlatform#1204 holds the local changes back for, as OpenIdentityPlatform#1202 records. ReplicationBrokerStopTest connects a domain to a replication server which keeps the session up and reads all of it, but gives a window of one update and no credit after it unless the test gives some. Disabling the domain once a change spent the window ends in bounded time, the replica offline message not written, and so does disabling it while a second change waits for credit under the lock, the change ending with it (both red before the change). A change given up is followed by nothing, even with credit coming right after it, and a broker started again writes again (red before the change, where the change waits for credit for good, and on a version which gave up after a single wait for credit, where the replica offline message went out after it). Credit given a second into the stop gets that change and then the replica offline message out (green before the change, red on that version). A broker started again after a stop waits for credit past the deadline of that stop, and a stop with the window open writes the replica offline message (green before the change: they pin what the change keeps). A stop whose replica offline message fails still stops the broker (red before the change). Each of eight mutants is caught: stopping or the give-up not cleared by start(), stopping set only after the replica offline message, no deadline, no give-up kept, a stopping broker writing nothing, only the replica offline message given up, and no finally around it.
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-3 fixes are where the bugs were, and the suite is green.
synchronize()takes a local delete that did not go through out ofreplayOperationsbeforependingChanges.remove()/pushCommittedChanges()let the later changes out (LDAPReplicationDomain.java:2700-2718).ReplicationBroker.publish()now refuses only anUpdateMsgwhile a recovery is required (ReplicationBroker.java:2372), and a recovery round'sNO_SUCH_OBJECTcounts as gone through only withbaseEntryIsAbsentFromALiveBackend()(LDAPReplicationDomain.java:7475).HistoricalCsnOrderingTest(51 cases) andCSNGeneratorTest(6 cases) pass at f7fcf27.
issue (non-blocking): A modify or add that a recovery kept is dropped at enable() when the state save in disable() did not write, even though the entry holds the change.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:5810-5817, :5604-5605, opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PersistentServerState.java:140
forgetTheLocalChangesTheDataLacks() drops every kept non-delete change newer than the max CSN of the state it reads back. disable() wrote that state with state.save(). That call returns without writing when the checkpointer's write holds saveLock (disable() does not stop ServerStateFlush), or when updateStateEntry() fails. clearInMemory() then throws away whatever was not written. So a kept modify C that is missing from the saved state gets dropped, although its entry holds it. Now let a local change D come in while the recovery that spans the reload is still running: the round publishes only D, recoveryUntil(D) ends the recovery, and C never reaches a replication server. BASE did publish C, through its windowed search up to D. The give-up in disable() is older than this PR (it dates from #948). The new drop at reload is what turns it into a lost change. "A domain enabled back on its own data loses none" (:5782-5783, and the commit message) is only true when that save wrote. Not run: no test makes the save in disable() lose that race. Also not traced: whether a kept change can be committed after the in-flight write took its snapshot.
Or: at least narrow the claim to a disable whose save wrote. Closing the gap needs one of two changes. Either save() reports whether it wrote and disable() clears the state only once it has, or the drop is based on the data rather than on the max CSN of the saved state.
suggestion (non-blocking): No test hands a session start over while a held round publishes the kept changes, so the !startHandedOver() conjunct before keepNoMoreChanges() is not pinned.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:7566, :7579, :7281
The TestBroker cases go through the @VisibleForTesting overload (:7281) with publisher == null, so for them startHandedOver() is always false. The cases with a real publisher hand the start over at other points: in round 2's search (HistoricalCsnOrderingTest.java:1455), after the run (:1528), in the backoff wait (:2178), or with setMaxKeptChanges(0) (:1594). Without the conjunct, a start that arrives during a held round makes the recovery keep no more changes for the rest of its life. The new session's run then searches the historical information, which does not rebuild a kept change whose values a later change replaced. Every case stays green under this mutant (traced by reading, not run):
if (!endedRecovery && heldBack)Pin: take aChangeWhoseValuesALaterOneReplacedIsPublished's setup and run it through a real publisher (setMissingChangesPublisherRunHook, kept changes on). From session 1's publishRecovery, while the held round publishes the kept changes, start a session to a second replication server that holds less. Assert that the second replication server gets the replaced change.
issue (non-blocking): The comment on Recovery.keep() and the LDAPUpdateMsg.setNotAssured() Javadoc say a kept change is published once its operation has given up waiting for the acknowledgement. Nothing enforces that order.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChanges.java:516-525, opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/LDAPUpdateMsg.java:209, LDAPReplicationDomain.java:2659-2679
keep() runs inside commitAndPushCommittedChanges() (LDAPReplicationDomain.java:2663), before the operation enters waitForAckIfAssuredEnabled() (:2675). The rounds publish kept changes without looking at waitingAckMsgs or the assured timeout. With assured replication on, a kept change can therefore go out, not assured, while its operation is still waiting. No acknowledgement comes back, so the operation waits out the whole assured timeout and logs NOTE_DS_ACK_TIMEOUT. That outcome is the same as BASE, which published nothing during a recovery. What is wrong is the stated order, which appears in the comment, the Javadoc and the commit message.
/*
* Published as a recovery publishes what it rebuilds: not assured. Its operation may still be
* waiting for the acknowledgement when a round publishes it; it then gets none and times out,
* as it waits as the domain is configured to, on the CSN of the message, and not as the message says.
*/Or: keep the message assured while its CSN is still in waitingAckMsgs. The operation could then return on the acknowledgement.
question (non-blocking): (round 2) Does #1202's fix — bounding the held publish, or letting stop() in — also cover replays that wait on the entry locks of local operations parked in a held round?
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChanges.java:588, LDAPReplicationDomain.java:7248
A local operation parked in holdBackLocalChangesWhile() keeps its entry lock. A replay of the same entry gets BUSY after LockManager's 9 s, and after 10 attempts it gives up. This is a Minor. It becomes a Major if a held round can outlast ~90 s without a withheld window or a reconnect. I have not re-derived it at this head, where the hold covers only the publish of the kept changes.
nitpick (non-blocking): ERR_MISSING_CHANGES_NOT_PUBLISHED_333 gives the backoff as the delay before the next attempt, but a session start that is handed over ends the wait early.
opendj-server-legacy/src/messages/org/opends/messages/replication.properties:722-724, logged at LDAPReplicationDomain.java:949
waitForAStartOrTheBackoff returns as soon as a session hands its start over. It returns at once if the hand-over came during the failed run. The number is an upper bound.
ERR_MISSING_CHANGES_NOT_PUBLISHED_333=Domain "%s" could not publish the changes its replication \
server misses: %s. It tries again within %d ms - on the session which starts meanwhile, or on one it \
starts then - and the changes done meanwhile are left to that recovery as well…y runs, and end the recovery only once they are out When a directory server connects to a replication server which misses some of its changes, the missing change publisher (RSUpdater, buildAndPublishMissingChanges()) republishes them from the historical information of the entries, while the local changes done meanwhile are only recorded in the domain state and left to it. Under load it lost such changes three ways; the one switch of replication server in the run of OpenIdentityPlatform#1195 lost 81 changes of cv-c3 this way (21 adds, 2 deletes, 58 modifications), not the two deletes the convergence check saw. - A round chose what to publish by the domain state read after its search. A change done while the search ran was missing from what it found but covered by that state, so a later change was published in its place and the next round searched from that later change on: the change in between was never published. Deletes escaped this only because they are kept aside as they are done. A round now publishes up to the last local change read before its search - a change done by then has its entry written - and the next round starts from there. - A round which found nothing it could publish jumped ten seconds ahead, which ended the recovery whatever had been done meanwhile; the changes pushed between its search and that end were left to a recovery which was over, and the deletes kept aside were cleared. The jump is gone: a round never goes past the last local change it was bounded by. - The recovery let the pending changes publish again before the broker took them: the broker flag was cleared only when the thread ended, and a change pushed in between was refused while the domain state recorded it as done. Both are now switched together, under the lock the pending changes are pushed under. Bounding a round by the changes done when it begins means a recovery could chase the local changes for as long as they go on, so once a round has caught up with them and some were done while it ran, the rounds after it hold them back up to the last local change, and the recovery ends with them. The local operations wait for these rounds under the lock a publish in normal operation takes for one change. The rounds now start where a search window ended much more often than the old jump did, and the end of a window carried the sequence number 0xffffffff: -1 in a CSN, and the greatest in the text the index of the historical information compares. A change of the last millisecond of a window was then in neither that window nor the next one. The end now carries Integer.MAX_VALUE, which reads the same in both. A round also counts as published only what the broker wrote to a session, which publishRecovery() now reports: when the broker refuses a change, the recovery fails and the changes left are kept for the recovery of the next session, rather than the recovery ending without them and the deletes kept aside being dropped. The search also handed over the changes of the other replicas found in the historical information of an entry, as CSNs are ordered by time first. The publisher now keeps to the changes of its own replica, as the search was meant to: a replication server takes no change of a replica older than the newest one it holds, so one it had from here first would make it drop the older changes of that replica which reach it afterwards. Fixes OpenIdentityPlatform#1195
…refused publisher left, and pin the hand-over to the broker Review round 1: - A recovery whose change the broker refused - it reached no replication server, or it was stopped - left the pending changes in recovery, and a session which needed no recovery of its own, its replication server missing no change of this replica, left them there: every local change of that session was recorded in the domain state and published by nobody. Such a session now starts a missing change publisher all the same; it finds nothing to publish, or the changes done since, and ends the recovery under the lock of the pending changes. The recovery is not ended in sessionInitiated() itself: that runs under the connect phase lock of the broker, which a change being pushed waits for holding the lock of the pending changes. - theBrokerTakesChangesAgainUnderTheLockTheyArePushedUnder pins that the broker takes changes again under that lock, and aStoppedBrokerRefusesARecoveryMessage that publishRecovery() reports a refusal. - assertEachChangePublishedOnce checks that the changelog holds, after the recovery, the changes the broker published and nothing else, so that no change of the recovery goes out twice. - aChangeInTheLastMillisecondOfASearchWindowIsPublished opens the session of the other replica first, so that a slow start of it cannot push the change past the window. - The javadoc of publishRecovery() and of the rounds says what a refusal is.
…again, publish nothing of a failed search, and keep the deletes kept aside Review round 2: - A recovery round whose search did not go through, on a session which stays up, left the pending changes in recovery with no publisher: every local change was recorded in the domain state and published by nobody until the session went down for another reason (OpenIdentityPlatform#1197). Ending the recovery there instead would have the next local change published over the changes the recovery had not published, which the replication server then never takes. The publisher now has the session started again, and the recovery of the next session publishes them: it logs ERR_MISSING_CHANGES_NOT_PUBLISHED, waits with the session up - one second per failure in a row, up to ten - lets go of the recovery, and asks for a session restart, which the state checkpointer runs. A round which throws is handled the same way. - A round published what its search had found before it read how the search ended (OpenIdentityPlatform#1201): after a search which failed part-way, the replication server held a change newer than the ones the search missed, and took none of them afterwards. The result is read first now: a failed search publishes nothing and takes nothing out of the changes kept aside, and a round which throws while publishing puts back what it had not published. - A search which finds no base entry is not a failed one: the suffix is gone, no entry below it holds a change, and the deletes kept aside are all there is. They go out and the recovery ends, rather than waiting for the suffix to come back. - The publisher of a session whose replication server holds no change of this replica trimmed the deletes kept aside up to the newest change of this replica, and the end of its recovery cleared them: a later session to a replication server which missed one of them had nothing to publish it from. The deletes at or before where a round starts are now skipped and left in place, and the list is not cleared when a recovery ends; the cap of 10000 still bounds it (OpenIdentityPlatform#1200). - The search listener takes a change numbered exactly the end of its window, as the search filter does: the next window starts after it, and the change was in neither. - A change the broker refuses no longer logs ERR_CANNOT_RECOVER_CHANGES, which blamed a search: the next session publishes it, and a debug line says so. - The comment of the new arm of sessionInitiated() no longer says the compare-and-set cannot lose: a publisher on its way out after a refusal can still hold it (OpenIdentityPlatform#1199). - New tests: a round whose search failed publishes nothing; the deletes a round threw before publishing are left to the next recovery; a recovery whose search failed, and one which threw, have the session started again and lose no change; the deletes of a suffix removed during a recovery are published; the deletes kept aside outlive a session to a replication server which holds no change of this replica; a recovery left on a replica with no change of its own ends; a change numbered the end of a search window is published. The test of the changes the broker refused has the delete refused first, and the replication servers of a test are forgotten after it.
…d the start of a new session to the publisher which runs, and keep only the deletes the data reflects Review round 3: - A local delete which did not go through - its conflicts were resolved, and then the access control, a plugin or the backend refused it - stayed among the deletes kept aside, and a later recovery published it: the replicas it reached deleted an entry this one keeps. It is taken out in the failure branch of synchronize(), before the pending changes let the later changes out, so no round can reach past it while it is still listed. - A recovery whose search failed waited for the session restart with the broker flag up, and the broker refused every other message meanwhile. The broker refuses the UpdateMsgs only while a recovery is required now (OpenIdentityPlatform#1220, below). The announcement that the replica goes offline, the other UpdateMsg, is given up on by the pending changes for as long as a recovery runs: it carries the newest CSN of this replica, and the replication server took the replica for offline from there, its change number index past the changes which reached it afterwards - where the flag was down, as for a recovery a refused publisher left, or the stop of the session restart a failed one asks for. - A search which answered NO_SUCH_OBJECT counted as one which found everything even when the base DN had no backend - one an import took away - and its round moved the recovery past the changes of its window. It counts so only when the backend is there and holds no base entry, as findEntryDN() reads it. - A session which hands its start over to a publisher which waits after a failed run ends the wait, and a publisher which throws an error, which no catch of an exception takes, has the session started again as well. - New tests: a recovery which keeps failing waits longer before each restart, and forgets its failures once it succeeds; a session which starts while a failed recovery waits is recovered at once; a recovery which threw an error has the session started again; the replica is not announced offline while a recovery has changes to publish. Scope extended to OpenIdentityPlatform#1199, OpenIdentityPlatform#1200, OpenIdentityPlatform#1203, OpenIdentityPlatform#1220, OpenIdentityPlatform#1221 and OpenIdentityPlatform#1223: - OpenIdentityPlatform#1203: while a recovery runs, the pending changes keep the messages of the local changes they push, in order, rather than dropping them and leaving the recovery to rebuild them from the historical information of the entries. The search of the recovery only covers the changes pushed before the first one it kept, up to a CSN no commit moves, and the kept messages go out next. The round which ends the recovery holds the local changes back only to publish the ones pushed during the round before: no search runs under the hold. A change whose values a later change replaced is published too, where the historical information keeps the later one only. Past 10000 changes or 32 MiB, the recovery keeps none and searches for them from where it has got to, as it did before; so does a recovery whose round, holding the local changes back, does not reach the last local change with what it kept, rather than go round for ever under the lock every local write waits for. A kept change goes out as not assured, as the changes a recovery rebuilds do: an operation which still waits for its acknowledgement gets none and times out, as it does when the change goes out later (LDAPUpdateMsg.setNotAssured() drops the cached encoding). A recovery publishes nothing while a total update replaces the data of this replica, as what it would publish was done on the data the import replaces. - OpenIdentityPlatform#1199: a session which starts while the missing change publisher of the previous one runs hands it its start - the newest change of this replica its replication server holds - and its session, and the publisher goes back there and publishes on that session. ReplicationBroker .publishRecovery() takes the session the recovery is for, and refuses the message for any other: a run for the session before cannot get a later change to the new replication server ahead of the older ones it misses. The start of a recovery, its end, the hand-over and the exit of the publisher are decided under one lock, which is taken inside the connect phase lock of the broker and inside the lock of the pending changes, and under which neither is taken: a recovery is not ended while a start is handed over, and the publisher does not leave before it has run from it. A publisher which failed and waits runs from the start of a session which comes meanwhile, rather than asking for a session restart. - OpenIdentityPlatform#1200: what the search of a round finds is the round's own, and the list of deletes kept aside holds deletes only: a delete done while a round runs no longer drops a change the round has just found. The deletes a round publishes stay in the list, for the replication server of a later session. A delete no replication server has taken, which the cap of 10000 drops, logs ERR_UNPUBLISHED_DELETE_DROPPED (ordinal 334). - OpenIdentityPlatform#1220: while a recovery is required, the broker refused every message which was not a recovery one - the acknowledgements of replayed changes, the messages of a total update, the monitoring requests, the reset of the generation ID. It refuses the UpdateMsgs only now: the changes of this replica, and the announcement that it goes offline. - OpenIdentityPlatform#1221: once the ServerState is read from the data again - as the domain is enabled, or after a total update into it - the deletes kept aside and the local changes a recovery kept which the data does not reflect are dropped: it was restored or imported since, and a recovery which published them would have the other replicas delete or change entries this one holds as they were. What comes after the newest change of this replica that state holds is not in the data, except for a delete, which leaves no historical information, and which the state misses as well when it could not be saved - a delete of the base entry takes the saved state with it: a delete is dropped only if its entry is in the data. A domain enabled back on its own data loses none whose state, read back, holds a change of this replica: PersistentServerState brings that state up to the changes the historical information of the entries holds, whether or not the last save wrote them. - OpenIdentityPlatform#1223: a domain which read back a ServerState with no change of its own - the delete of the base entry took the saved state with it, the backup it was restored from was taken before the state was saved, or the data was imported - had CSNGenerator.adjust(null) move its generator back to the clock. The next local changes were numbered as, or before, ones this replica had numbered, which the replication server of the session missed and a recovery still had to publish - one of two changes on a CSN is lost - and before the changes of other replicas it had replayed. An adjust with no CSN leaves the generator where it is now, and the tests of the deletes of a suffix no longer wait for the clock to pass the deletes. - Test hooks: LDAPReplicationDomain.setMaxDeletesKeptAside(), LDAPReplicationDomain.setMissingChangesPublisherRunHook(), PendingChanges.setMaxKeptChanges(). - New tests: a change whose values a later one replaced is published; no search runs while the local changes are held back; a recovery which kept too many changes searches for them and loses none; a recovery which keeps the local changes ends while they go on and loses none; a recovery whose kept changes miss one searches for it; a kept change is published as not assured; a recovery publishes nothing while a total update replaces the data; the local changes a recovery kept outlive a disable, and go with the data they were done on; the deletes kept aside go with the data they were done on; the deletes of a suffix outlive a state which could not be saved, the ones a recovery kept as well; a broker refuses a recovery message for a session it replaced; a broker with a recovery to run refuses the changes only; a session which starts while a recovery runs gets every change its replication server misses; a session which starts as the publisher leaves gets a recovery of its own; a recovery a session which started meanwhile needs is not ended; a search which finds no backend fails its round; the changes a round finds take no room among the deletes kept aside; a delete no replication server took is not dropped without a trace; a delete which failed is not published by a recovery; a replica which reads back a state with no change of its own numbers its next change after its last one; an adjust of the CSN generator with no CSN numbers before neither the CSNs it generated nor the ones it received. The tests of the search rounds start the recovery after the changes they search for, or run with a recovery which keeps no local change; three of them run both ways.
… this replica up to the changes a recovery kept, and pin the start a session hands over while the local changes are held back Review round 4: - The local changes a recovery kept were dropped as the domain was enabled back on its own data when the state it read back held no change of this replica. A state which the save on the way down did not write is brought up, as it is loaded, to the changes the historical information of the entries holds past the newest change of this replica it holds (PersistentServerState.checkAndUpdateServerState()), so a kept change whose entry holds it is covered and kept. A state which holds none is left as it is, as that search would go through every entry the replica ever changed. Such a state is read back once the base entry, which holds the saved state, was deleted and added back, and no save wrote the state into it since: the add of the base entry was dropped, and the recovery published the deletes which removed the suffix alone, so the other replicas lost it. loadDataState() now brings such a state up to the changes of this replica the historical information holds from the first change the recovery kept. A search which does not go through logs ERR_CANNOT_RECOVER_CHANGES, and the kept changes are dropped then, as when the search of PersistentServerState does not go through. - The comment of PendingChanges.Recovery.keep() and the Javadoc of LDAPUpdateMsg.setNotAssured() said a kept change goes out once its operation has given up waiting for the acknowledgement. Nothing orders the two: an operation which still waits gets no acknowledgement, and times out as it does when a round publishes the change later. - ERR_MISSING_CHANGES_NOT_PUBLISHED gives the wait before the next attempt as the longest it takes: a session which hands its start over ends it. - Test hook: LDAPReplicationDomain.setKeptChangesPublishedHook(), run by a round which publishes the local changes the recovery kept, once it has published them and before it ends the recovery. - New tests: the local changes a recovery kept outlive a disable whose save did not write the state; a suffix deleted and added back during a recovery outlives a state which could not be saved into the base entry added back; a session which starts while a round holds the local changes back to publish the ones the recovery kept gets them all. Its session comes up as the broker reconnects after its replication server went away, which takes no lock of the pending changes: a round which took the changes it kept for short of the last local change then had the recovery keep none, and the run for the new session searched the historical information, which holds nothing of a change whose values a later one replaced.
f7fcf27 to
0a1cdf7
Compare
|
Round 5: 0a1cdf7. The branch is rebased onto master (616d5f6, whose change since round 4 touches issue (non-blocking): a kept change dropped at The gap is narrower, and real: suggestion (non-blocking): the issue (non-blocking): the stated order of a kept change and the end of the wait for its acknowledgement. Confirmed: question (non-blocking): #1202 and the replays waiting on the entry locks of local operations parked in a held round. As in round 2: no, #1202 and #1210 bound nitpick (non-blocking): Tests: |
…e to give credit once the broker is being stopped, and write nothing after a message given up ReplicationBroker.stop() published the replica offline message before it set shutdown, the one flag which ends the wait of a publish for window credit. That message needs credit as an update does, and it is published under the lock of the pending changes, which a local change holds while it is pushed - and while its publish waits for credit. Credit comes only as the replication server takes the updates of the session, while its heartbeats come from a thread of their own and keep the session up: a replication server which took no update held the stop for as long as it took none, and with it ReplicationDomain.disableService(), which calls stop() holding serviceStateLock and sessionLock - the shutdown of the domain, an import, a restore, a session restart. With the window of the session spent, a stop waited for the credit of its own replica offline message even with no local change in flight. stop() now sets a stopping flag, with a deadline, before it publishes that message, and start() clears it. The replication server gets as long to give credit as it gets to accept a connection (the connection timeout of the replication provider, 5 seconds by default): one which is only slow still takes the changes and the replica offline message, as before. A publish which finds no credit once the deadline is past gives its message up - a local change, which the next session republishes from the historical information of its entry, or a delete from the operations the domain keeps in memory, which a restart of the server loses; or the replica offline message, which PendingChanges.putReplicaOfflineMsg() then reports as not sent, its announcement withdrawn - and from then on the broker writes nothing: written after a change given up, the replica offline message, which carries the newest CSN of the replica, would tell the replication server that nothing older is to come. stop() also stops the broker when publishing that message fails, rather than leave a stopping broker running. Not changed: a publish an IOException holds up still retries until the broker is connected again or finds no replication server to connect to, and a publish blocked in the write to the socket itself is not ended by the flag - with the default window of 100000 updates the socket buffers fill before the window closes, and that hang is OpenIdentityPlatform#1209. The recovery publishes given up are not reported to the recovery on master, as those refused once shutdown is set were not; OpenIdentityPlatform#1204 makes publishRecovery() report them. With the window open, a stop still waits for the end of a round OpenIdentityPlatform#1204 holds the local changes back for, as OpenIdentityPlatform#1202 records. ReplicationBrokerStopTest connects a domain to a replication server which keeps the session up and reads all of it, but gives a window of one update and no credit after it unless the test gives some. Disabling the domain once a change spent the window ends in bounded time, the replica offline message not written, and so does disabling it while a second change waits for credit under the lock, the change ending with it (both red before the change). A change given up is followed by nothing, even with credit coming right after it, and a broker started again writes again (red before the change, where the change waits for credit for good, and on a version which gave up after a single wait for credit, where the replica offline message went out after it). Credit given a second into the stop gets that change and then the replica offline message out (green before the change, red on that version). A broker started again after a stop waits for credit past the deadline of that stop, and a stop with the window open writes the replica offline message (green before the change: they pin what the change keeps). A stop whose replica offline message fails still stops the broker (red before the change). Each of eight mutants is caught: stopping or the give-up not cleared by start(), stopping set only after the replica offline message, no deadline, no give-up kept, a stopping broker writing nothing, only the replica offline message given up, and no finally around it.
maximthomas
left a comment
There was a problem hiding this comment.
praise: The new bring-up sits exactly where #1221's add of the base entry was lost, and both round-5 mutants the description names turn their tests red.
- Deleting
bringAStateWithNoChangeOfThisReplicaUpToTheData();atLDAPReplicationDomain.java:5773turnsaSuffixAddedBackDuringARecoveryOutlivesAStateWhichCouldNotBeSavedInItred (the add of the base entry is missing from the changelog). Dropping!startHandedOver()at:7659turnsaSessionWhichStartsWhileTheLocalChangesAreHeldBackGetsTheChangesTheRecoveryKeptred. Both measured at this head,HistoricalCsnOrderingTest54 tests. - In
docker-convergence (rs534)at this head, both replication servers' changelogs hold every change of every replica (changelog/cv-r0.txt,cv-r1.txtin the artifact: 0 missing), cv-d3 included after 29 reconnections.
…e to give credit once the broker is being stopped, and write nothing after a message given up ReplicationBroker.stop() published the replica offline message before it set shutdown, the one flag which ends the wait of a publish for window credit. That message needs credit as an update does, and it is published under the lock of the pending changes, which a local change holds while it is pushed - and while its publish waits for credit. Credit comes only as the replication server takes the updates of the session, while its heartbeats come from a thread of their own and keep the session up: a replication server which took no update held the stop for as long as it took none, and with it ReplicationDomain.disableService(), which calls stop() holding serviceStateLock and sessionLock - the shutdown of the domain, an import, a restore, a session restart. With the window of the session spent, a stop waited for the credit of its own replica offline message even with no local change in flight. stop() now sets a stopping flag, with a deadline, before it publishes that message, and start() clears it. The replication server gets as long to give credit as it gets to accept a connection (the connection timeout of the replication provider, 5 seconds by default): one which is only slow still takes the changes and the replica offline message, as before. A publish which finds no credit once the deadline is past gives its message up - a local change, which the next session republishes from the historical information of its entry, or a delete from the operations the domain keeps in memory, which a restart of the server loses; or the replica offline message, which PendingChanges.putReplicaOfflineMsg() then reports as not sent, its announcement withdrawn - and from then on the broker writes nothing: written after a change given up, the replica offline message, which carries the newest CSN of the replica, would tell the replication server that nothing older is to come. stop() also stops the broker when publishing that message fails, rather than leave a stopping broker running. Not changed: a publish an IOException holds up still retries until the broker is connected again or finds no replication server to connect to, and a publish blocked in the write to the socket itself is not ended by the flag - with the default window of 100000 updates the socket buffers fill before the window closes, and that hang is OpenIdentityPlatform#1209. The recovery publishes given up are not reported to the recovery on master, as those refused once shutdown is set were not; OpenIdentityPlatform#1204 makes publishRecovery() report them. With the window open, a stop still waits for the end of a round OpenIdentityPlatform#1204 holds the local changes back for, as OpenIdentityPlatform#1202 records. ReplicationBrokerStopTest connects a domain to a replication server which keeps the session up and reads all of it, but gives a window of one update and no credit after it unless the test gives some. Disabling the domain once a change spent the window ends in bounded time, the replica offline message not written, and so does disabling it while a second change waits for credit under the lock, the change ending with it (both red before the change). A change given up is followed by nothing, even with credit coming right after it, and a broker started again writes again (red before the change, where the change waits for credit for good, and on a version which gave up after a single wait for credit, where the replica offline message went out after it). Credit given a second into the stop gets that change and then the replica offline message out (green before the change, red on that version). A broker started again after a stop waits for credit past the deadline of that stop, and a stop with the window open writes the replica offline message (green before the change: they pin what the change keeps). A stop whose replica offline message fails still stops the broker (red before the change). Each of eight mutants is caught: stopping or the give-up not cleared by start(), stopping set only after the replica offline message, no deadline, no give-up kept, a stopping broker writing nothing, only the replica offline message given up, and no finally around it.
…ng the deletes a stop gives up for the next session, now that OpenIdentityPlatform#1204 keeps it aside After the rebase onto d954c4c, past OpenIdentityPlatform#1204: - The recovery takes the deletes it publishes from those the domain keeps aside without removing them, and marks one published only once the broker wrote it (OpenIdentityPlatform#1204). A delete the recovery publishes and a stop gives up therefore stays aside, for the recovery of the next session, and only a restart of the server loses it, as any local delete. The comment of the give-up in publish() still said it had left them already and was lost.
… give credit and to read once the broker is being stopped, and write nothing after a message given up (#1210) Fixes #1202 Fixes #1209 Fixes #1222 ## Summary `ReplicationBroker.stop()` could not end while its replication server kept the session up and took no update from it, and with it hung `ReplicationDomain.disableService()`, which calls `stop()` holding `serviceStateLock` and `sessionLock` - the shutdown of the domain, an import, a restore, a session restart. It hung in two ways: waiting for window credit the replication server did not give (#1202), and, once the socket buffers were full, in a write to the session which the replication server did not read (#1209) - with the default window of 100000 updates, the buffers fill long before the window closes. The stop now gives the replication server a bounded time to give credit, and as long again to read what was written, after which it closes the session as a failed one; once a publish gave its message up for the stop, the broker writes nothing more. The same write held the session with no stop at all once the replication server was gone without a reset (#1222): the heartbeat monitor, which then closes the session, waited for it as well, until TCP gave up retransmitting. It now closes such a session as a failed one too. ## Root cause - The one flag which ends the wait of a publish for window credit is `shutdown` (`while (!done && !shutdown)` in `ReplicationBroker.publish()`), and `stop()` set it only after `domain.publishReplicaOfflineMsg()` returned. - `ReplicaOfflineMsg` extends `UpdateMsg`: it takes a permit of the send window like a change. With the window spent, the stop waited for the credit of its own replica offline message even with no local change in flight - wider than the case #1202 describes. - In the case of #1202, a local change waits for credit inside `PendingChanges.commitAndPushCommittedChanges()`, under the monitor `PendingChanges.putReplicaOfflineMsg()` needs: the stop waits for that change, which waits for credit. - Credit comes only as the replication server takes the updates of the session, while its heartbeats come from a thread of their own and keep the session up. Nothing else ended the wait. - A directory server writes its session on the thread which publishes - its `Session` runs no publisher thread - and a local change is published under the lock of the pending changes. A write which the replication server does not read blocks in the socket once the buffers are full, holding that lock, `connectPhaseLock` and the `publishLock` of the session (#1209). A socket write has no timeout, and `Session.close()` of a session with no error takes `publishLock` first: nothing on the directory server ended that write. - The heartbeat monitor, which closes the session once the heartbeats of the replication server stop, closed it with `Session.close()` too (#1222). A replication server which is gone without a reset acknowledges nothing: the writes fill the buffers, the next one blocks, and the close waited for it until TCP gave up retransmitting - while the listener stayed in a read which neither data nor a reset ends, and the local changes waited for the lock of the pending changes. ## Change `ReplicationBroker`: - `stop()` records the stop in progress, with a deadline, before it publishes the replica offline message: a `StopInProgress` in the `volatile` field `stopping`, which `start()` clears to `null`. The deadline is the connection timeout of the replication provider (`MultimasterReplication.getConnectionTimeoutMS()`, 5 seconds by default): the replication server gets as long to give credit as it gets to accept a connection. One which is only slow still takes the changes and the replica offline message, as before. A connection timeout of 0, which sets no limit to a connection or to an SSL negotiation, sets none to the stop either: the stop then waits as it did before. - A stop which overlaps one in progress keeps that one, its deadlines and what it gave up, rather than replacing it. - A publish which finds no credit once the deadline is past gives its message up and marks the stop (`StopInProgress.gaveUp`), which ends the publish loop for every later message as well, until `start()` clears the stop. Before it marks the stop it reads it again: if `start()` ran since, the loop head decides. What is given up: - a local change, which the next session republishes from the historical information of its entry - or a local delete, one the recovery publishes included, from the deletes the domain keeps aside in memory, which a restart of the server loses; - or the replica offline message, which `PendingChanges.putReplicaOfflineMsg()` then reports as not sent, its announcement withdrawn. - Nothing is written by `publish()` after a message given up: the replica offline message carries the newest CSN of the replica, and written after a change given up it would tell the replication server that nothing older is to come. - The give-up is marked on the stop, not on the broker. A publish which found the stop past its deadline may mark it only after `start()` cleared it - the read and the write are not atomic, and no lock is taken for them - and it then marks a stop which is over, rather than the broker started again, which would refuse every publish until its next stop. - `stop()` starts a watchdog for the stop, unless the connection timeout is 0 (`endWhatStillHoldsTheStop()`). If the stop is still held when it is past its credit deadline by as long again - twice the connection timeout from its start - what holds it is no wait for credit, which gives up by itself at the deadline, but a write the replication server does not read, or a lock such a write holds. The watchdog then marks the give-up of the stop, logs a warning which names the replication server, and closes the session as a failed one (`Session.abort()`): the write fails, and the publish which made it returns rather than retries. A stop which waits for credit and gives up still ends its session in order, with its `StopMsg`. - The watchdog acts under the monitor of the stop, and `stop()` ends the stop under `startStopLock` once it is done: the watchdog of a stop never closes a session of a broker started again. - A publish reads the give-up again under `connectPhaseLock`, before it writes a message and before it writes a window probe. A publish which passed the loop head before the give-up may wait for that lock behind a connection of the listener - one which the replication server ending the session in the stop set off, and which takes the connection timeout for each replication server that answers nothing - past the deadline of the watchdog, which then finds no session to close. Without that read the publish wrote to the session the connection opened, and a replication server which did not read it would hold the stop with nothing left to end it. The watchdog marks the give-up before it reads the session, and a connection sets its session before it releases the lock: either the watchdog closes that session, or the publish finds the mark. - Once the stop gave up, the broker opens no session again while the stop runs (`reStart()`): the listener, which the close of the session wakes, waits for the end of the stop rather than connects. A session opened then would carry nothing but its change time heartbeats - which tell the replication server that nothing older is to come from this replica, while it still has the changes given up - and the stop would wait for the connection. A connection already under way when the stop gives up still opens its session: `publish()` writes nothing to it, and the stop closes it as it ends. - `stop()` stops the broker in a `finally`: a failure of the replica offline message no longer leaves a stopping broker running, which would give up every publish that waits for credit. The watchdog is started inside the same `try`: a thread which cannot be started leaves no stopping broker running either. `Session.abort()` (new) records the error, marks the close begun, closes the sockets, and then closes the session, which takes the branch of a failed session: it writes nothing more - neither what the publisher thread left queued nor the `StopMsg` - and takes no lock. The sockets closed first end a write blocked in them, whichever thread makes it - the publisher thread, which any close joins, included - and release a close which had begun before, with no error, and waits for the lock of the writes. The close is marked begun before them, so that the listener whose read their close ends finds it begun, and does not report it as a failure of the replication server. `HeartbeatMonitor` closes a session whose heartbeats stopped with `Session.abort()` (#1222): the blocked write and the read of the listener fail, and the reconnection proceeds as for any session which failed; no `StopMsg` is written, which a replication server whose heartbeats stopped is not expected to read. A monitor stopped with its broker still closes the session as before. The javadoc of `ReplicationDomain.publish(UpdateMsg)` and the trace of `LDAPReplicationDomain.publishReplicaOfflineMsg()` name the new reasons a message is not published, and the javadoc of `DSRSShutdownSync.replicaOfflineMsgNotSent()` counts the wait of a stopping broker in the window between an announcement and its withdrawal. The synopsis of `connection-timeout` (`ReplicationSynchronizationProviderConfiguration.xml`) says that it also bounds the wait of a stopping domain for credit, and its description says when a domain is stopped, what raising or lowering the value does to such a stop, that a replication server which does not read what was written gets as long again before the session is closed, and that 0 sets no limit. ## Not changed - A publish held up by an `IOException` still retries until the broker is connected again or finds no replication server to connect to - unless the stop gave up. - A recovery publish given up is refused to the recovery, as one refused once `shutdown` is set (#1204): the round ends, and the changes from it - a delete included, which stays aside - are left to the recovery of the next session. - An assured change given up for the stop still waits for its acknowledgment until the assured timeout, and is counted as sent and timed out. Ending that wait early needs a mark of its own in the assured machinery: a CSN taken out of the messages which wait for an acknowledgment reads as acknowledged. - A change given up comes back with the recovery of the next session, which runs in a thread of its own, and the change time heartbeats of that session do not wait for it. When the recovery takes longer than a heartbeat interval, the replication server takes a heartbeat as "nothing older is to come" from this replica, and the change gets its change number after newer changes of other replicas. Nothing is lost; any change a recovery publishes does the same on master. - The change time heartbeats, and the window and status messages written straight to the session, are not held back after a give-up: only `publish()` reads the mark, until the session is closed. That holds for the session a connection already under way at the give-up opens too, until the stop closes it as it ends. - A replication server which drops the session after a give-up for credit is reported (`WARN_REPLICATION_SERVER_BADLY_DISCONNECTED`), as with no stop at all: the give-up closes nothing. Only a close this server makes - by the watchdog of a stop, or by the heartbeat monitor - is not, as the close is marked begun before the sockets are closed. - A total update this domain is sending ends with an error once the stop gives up, as every message of it is refused - as it does on master as soon as a stop with the window open sets `shutdown`. Only the stop which waited for credit for good let it go on. - A session the watchdog or the heartbeat monitor closes ends with no `StopMsg`: the replication server sees the connection lost. - A reconnection of the listener to a better replication server (`reStart()` from `receive()`) still closes the current session with no error, and waits for a write blocked in it. - With the window open, a stop still waits for the end of a round #1204 holds the local changes back for, as #1202 records. ## Tests `ReplicationBrokerStopTest` (new) connects a domain to a fake replication server which keeps the session up and reads all of it, but opens a window of one update - or of the default 100000, in the cases of a write it does not read - and gives no credit after it unless the test asks it to. Like a replication server, it reports as its state the changes it received; it serves each connection on a thread of its own, and stops reading its sessions when the test asks it to. It sends the heartbeats the domain asks for, and can go away altogether: no reading, no heartbeat, no answer to a connection. It ends a session in order or drops it when the test asks it to. In the case of a connection under way, more replication servers take each connection and, once told to, answer nothing on it, as one which hangs does: a connection to each waits for the connection timeout in its SSL negotiation. Once the domain closed its session while the server read nothing, the server ends that session with a heartbeat, which the closed end answers with a reset: what the closed socket left queued, its end included, comes only once its TCP finds the window open again, and after a stall of 14 seconds its next window probe is 13 seconds away on Linux - the CI run of round 2 failed `aDisableEndsAWrite…` waiting for that end. A publish which waits for credit shows as a `WindowProbeMsg` on that server; "the stop waits for the lock" is the disabling thread `BLOCKED` in `PendingChanges.putReplicaOfflineMsg()`; a write it does not read is a writer which stays in `Session.send()` while the number of its changes stands still for two seconds in a row - half a second could be a pause of the collector, as a run after cases which allocate a lot showed; "reported as a close by the replication server" is a `WARN_REPLICATION_SERVER_BADLY_DISCONNECTED` in the error log; "the watchdog acted" is the end of its thread. The class runs with a connection timeout of 2 seconds instead of the 30 seconds `config-changes.ldif` gives the suite, and a case which needs another one sets it for its own run. | Case | What it pins | master | round 1 | |---|---|---|---| | `aDisableEndsWhenTheReplicationServerGivesNoCredit` | Once a change spent the window, disabling the domain ends, the replica offline message not written - no sooner than the connection timeout of 6 seconds the case sets, and within 22 seconds; the announcement of the message is withdrawn, and the session ends in order, with a `StopMsg`. | red | green | | `withNoConnectionTimeoutAStopWaitsForTheCredit` | With a connection timeout of 0, the replica offline message still waits for credit past its first wait and past 7 seconds, beyond the shipped default, and goes out once the credit comes. | green | green | | `aDisableEndsWhileALocalChangeWaitsForCredit` | Disabling while a second change waits for credit under the lock ends, and the change ends with it. | red | green | | `aStopPublishesWhatTheReplicationServerTakesInTime` | Credit given a second into the stop gets that change out, and then the replica offline message, last. | green | green | | `nothingIsWrittenAfterAChangeTheStopGaveUp` | A change given up is followed by nothing, even with credit offered right after it; a broker started again republishes it with the recovery of its session, and then writes again. | red | green | | `aSessionRestartRepublishesTheChangeTheStopGaveUp` | A restart of the session (`restartService()`) gives the change up as a disable does, and the recovery of the next session republishes it. Unlike `enable()`, which loads the state again and repairs it from `ds-sync-hist`, a restart keeps the state in memory: the change is found missing only because `ReplicationDomain.publish()` put it in the state although the broker did not write it. | red | green | | `aStopWritesNothingAfterAGiveUpEvenWithCredit` | A stop which gave a change up refuses the next one with the window open again, and an `EntryMsg` and a `DoneMsg`, which need no credit: no race with the 500 ms wait for credit. | red | green | | `aStopWhichOverlapsOneInProgressKeepsItsGiveUp` | After a change given up, the first stop writes nothing more, with the window open again, while a second stop runs beside it. | red | red | | `aBrokerStartedAgainWaitsForCredit` | A stop with the window open writes the replica offline message; a broker started again waits for credit past twice the connection timeout of that stop, and keeps its session. | green | green | | `aStopWhichCannotPublishTheOfflineMessageStillStopsTheBroker` | A stop whose replica offline message fails rethrows the failure and still stops the broker. | red | green | | `aDisableEndsAWriteToAReplicationServerWhichReadsNothing` | A local change blocked in its write under the lock of the pending changes, with credit to spare: disabling ends, no sooner than twice the connection timeout of 6 seconds and sooner than two and a half times it; nothing is written after the change, no session is opened again during the stop, nothing reports the close as one by the replication server, and the recovery of the session after `enable()` republishes the change. | red | red | | `aStopEndsItsOwnWriteAndOpensNoSessionAgain` | A write of the stop itself, with no lock of the pending changes in the way, ends the same way, no sooner than twice the connection timeout; the listener the close wakes waits for the end of the stop rather than connects, nothing reports the close as one by the replication server, and nothing is written after the write given up. | red | red | | `theHeartbeatMonitorEndsAWriteToAReplicationServerWhichIsGone` | A local change blocked in its write to a replication server which then reads nothing, sends no heartbeat and answers no connection: the write ends, no sooner than two heartbeat intervals, nothing reports the close as one by the replication server, and once the server is back the domain connects again and its change reaches it. | red | red | | `aConnectionUnderWayWhenTheStopGivesUpGetsNothingWritten` | The replication server ends the session in the stop, and four more which answer nothing hold the connection of the listener for the connection timeout each. The watchdog acts at its deadline with the connection still under way; an entry the stop publishes, which waits for the lock of that connection, is not written to the session the connection opens, and the stop ends once the connection does. | red | red | | `aDropByTheReplicationServerAfterAGiveUpIsStillReported` | After a change given up for credit, a replication server which drops the session with no `StopMsg` is reported, and the stop ends. | red | n/a | The fake replication server drops a session with `Session.abort()`, which round 1 does not have: `aDropBy…` cannot run there. On master its stop waits for credit and does not end. Round 2 (`26f7da4439`) fails `aDropBy…` and `aConnectionUnderWay…`, and round 3 (`4d1130e44e`) fails `aConnectionUnderWay…`, and only it. `SessionTest` gains three cases, red on master and on round 1, which have no `Session.abort()`; round 2 fails the third: | Case | What it pins | |---|---| | `anAbortEndsAWriteThePeerDoesNotReadAndACloseWhichWaitsForIt` | A write on the publishing thread, which a peer that reads nothing holds, and a close which waits for the lock of the writes behind it, both end with an abort; the peer finds the message cut short and the end of the stream, with no `StopMsg`. | | `anAbortEndsAWriteOfTheThreadOfTheSession` | A write of the thread of the session, which any close joins, ends with an abort, and the thread with it. | | `anAbortMarksTheCloseBegunBeforeItClosesTheSockets` | The socket of the session records, as it is closed, whether the close was marked begun by then: an abort marks it before it closes the sockets, so that the listener whose read their close ends finds it begun. | Mutants, each run against the classes of round 4 (`d3a6c33741`): | Mutant | Caught by | |---|---| | the stop not cleared by `start()` | `aBrokerStartedAgainWaitsForCredit`, `aDisableEndsAWrite…`, `aSessionRestart…`, `nothingIsWritten…` | | the stop recorded only after the replica offline message | `aConnectionUnderWay…`, `aDisableEndsAWrite…`, `aDisableEnds…NoCredit`, `aDisableEnds…WaitsForCredit`, `aDropBy…`, `aSessionRestart…`, `aStopEndsItsOwnWrite…`, `aStopWhichOverlaps…`, `aStopWritesNothing…`, `nothingIsWritten…` | | no deadline: give up at the first wait without credit | `aDisableEnds…NoCredit`, `aStopPublishes…`, `withNoConnectionTimeout…` | | give-up not kept for the later messages | `aStopWhichOverlaps…`, `aStopWritesNothing…`, `nothingIsWritten…` | | a stopping broker writes nothing at all | `aBrokerStartedAgainWaitsForCredit`, `aConnectionUnderWay…`, `aDisableEnds…NoCredit`, `aStopEndsItsOwnWrite…`, `aStopPublishes…`, `withNoConnectionTimeout…` | | only the replica offline message given up | `aDropBy…`, `aStopWhichOverlaps…`, `aStopWritesNothing…` | | no `finally` around the replica offline message | `aStopWhichCannotPublish…` | | a connection timeout of 0 gives up at the first wait (round 0) | `withNoConnectionTimeout…` | | a connection timeout of 0 read as the shipped 5 seconds | `withNoConnectionTimeout…` | | a constant deadline of 5 seconds instead of the connection timeout | `aConnectionUnderWay…`, `aDisableEndsAWrite…`, `aDisableEnds…NoCredit`, `withNoConnectionTimeout…` | | a constant deadline of 30 seconds | `aConnectionUnderWay…`, `aDisableEndsAWrite…`, `aDisableEnds…NoCredit`, `aDisableEnds…WaitsForCredit`, `aDropBy…`, `aSessionRestart…`, `aStopEndsItsOwnWrite…`, `aStopWhichOverlaps…`, `aStopWritesNothing…`, `nothingIsWritten…` | | `ReplicationDomain.publish()` puts in the state only what the broker wrote | `aSessionRestart…` | | an overlapping stop replaces the one in progress | `aStopWhichOverlaps…` | | the give-up of a publish reported as a message written | `aDisableEnds…NoCredit` (the announcement is left) | | after a give-up, only the updates refused | `aConnectionUnderWay…`, `aStopEndsItsOwnWrite…`, `aStopWritesNothing…` | | no watchdog (round 1) | `aConnectionUnderWay…`, `aDisableEndsAWrite…`, `aStopEndsItsOwnWrite…` | | the watchdog closes the session but gives nothing up | `aConnectionUnderWay…`, `aDisableEndsAWrite…`, `aStopEndsItsOwnWrite…` (a session opened again) | | the watchdog acts at the credit deadline | `aDisableEndsAWrite…`, `aDisableEnds…NoCredit` (no `StopMsg`), `aDropBy…`, `aStopEndsItsOwnWrite…`, `aStopWhichOverlaps…`, `aStopWritesNothing…` | | the watchdog acts whether the stop ended or not | `aBrokerStartedAgainWaitsForCredit` | | a watchdog with a connection timeout of 0 | `withNoConnectionTimeout…` | | the write deadline a constant 5 seconds past the credit deadline | `aConnectionUnderWay…` (the watchdog had not acted), `aDisableEndsAWrite…` | | the write deadline three times the connection timeout from the start of the stop | `aConnectionUnderWay…` (the watchdog had not acted), `aDisableEndsAWrite…` | | the write deadline two and a half times it | `aConnectionUnderWay…` (the watchdog had not acted), `aDisableEndsAWrite…` | | a session opened again once the stop gave up | `aDisableEndsAWrite…`, `aDropBy…`, `aStopEndsItsOwnWrite…` | | no read of the give-up under `connectPhaseLock` before the write, as round 3 did | `aConnectionUnderWay…` | | the listener reports no drop by the replication server once the stop gave up, as round 2 did | `aDropBy…` | | `Session.abort()` a plain close | `anAbortEndsAWriteThePeer…`, `anAbortEndsAWriteOfTheThread…`, `aDisableEndsAWrite…`, `aDropBy…`, `aStopEndsItsOwnWrite…`, `theHeartbeatMonitorEnds…` | | `Session.abort()` closes the session before the sockets | `anAbortEndsAWriteOfTheThreadOfTheSession` | | `Session.abort()` leaves the sockets to the close | both `SessionTest` cases of the abort | | `Session.abort()` leaves a close which had begun before to wait | `anAbortEndsAWriteThePeerDoesNotReadAndACloseWhichWaitsForIt` | | the heartbeat monitor closes a session whose heartbeats stopped as one with no error, as round 2 did | `theHeartbeatMonitorEnds…` | | `Session.abort()` closes the sockets before it marks the close begun, as round 2 did | `anAbortMarksTheCloseBegun…` | | no second read of the stop before its give-up is marked | not caught - see below | | a window probe written after the give-up | not caught - see below | Under the mutant which gives up only the replica offline message, a local change which waits for credit under the lock never gives up, and the watchdog ends it at twice the connection timeout: that is why that mutant no longer fails the cases where such a change holds the stop, as it did in round 1. The mutant "the state only what the broker wrote" survives a disable followed by `enable()`: `enable()` loads the state again, and `PersistentServerState.checkAndUpdateServerState()` repairs the CSNs of this replica from `ds-sync-hist`. A restart of the session keeps the state in memory, and only there does the mutant lose the change. Two races are not pinned, as a test cannot put a thread between the two steps without a hook: a give-up marked after `start()` cleared the stop it was read from - the mark lives in the object of that stop, and the second read of the stop narrows the window to that of master's loop head - and a watchdog acting after the end of its stop, which it does under the monitor `StopInProgress.end()` takes, under `startStopLock`. Nor is the start of the watchdog inside the `try`: a thread which fails to start needs a hook as well. The read of the give-up before a window probe is not pinned either: the probe is a few bytes which neither hold the stop nor tell the replication server anything of the changes, and the read only keeps `publish()` from writing anything after a give-up. ## Verification - `ReplicationBrokerStopTest` and `SessionTest` on their own at round 4 (`d3a6c33741`, on `616d5f6c17`), on macOS: 15/15 four times and 6/6 three times. Master fails 12 cases of the first - all but `aBrokerStartedAgain…`, `aStopPublishes…` and `withNoConnectionTimeout…` - and the 3 cases of the abort of the second. Round 1 (`8fb5973e30`) fails the 4 earlier cases the table marks red for it, `aConnectionUnderWay…`, and `aDropBy…`, which cannot run there, and the 3 cases of the abort. Round 2 (`26f7da4439`) fails `aConnectionUnderWay…`, `aDropBy…`, `theHeartbeatMonitorEnds…` and `anAbortMarks…`. Round 3 (`4d1130e44e`) fails `aConnectionUnderWay…` only. Every mutant above is caught, but the two noted; the runs a port collision with a test run of another worktree voided were run again. - On Linux in a container (JDK 21): `ReplicationBrokerStopTest` 15/15 twice, and `aConnectionUnderWay…`, `aDropBy…` and `aDisableEndsAWrite…` ten times more. - `ReplicationSynchronizationProviderConfiguration.xml` validates against `admin.xsd`. - Rebased onto `d954c4c53f` (#1204, #1211, #1219 and #1218 came in), at `276317c33a`, whose commit on top of round 4 changes a comment only; on macOS with JDK 21: `ReplicationBrokerStopTest` 15/15 and `SessionTest` 6/6, twice each, and the neighbours which share `publish()`: `ReplicationBrokerTest` (#1218) 37/37 and `HistoricalCsnOrderingTest` (#1204) 54/54.
…issing change publisher of the recovery before has left (#1226) ### Problem `HistoricalCsnOrderingTest#aSessionWhichStartsWhileARecoveryRunsGetsEveryChangeItsReplicationServerMisses` (added in #1204) failed once on master: run [39382279910](https://github.com/OpenIdentityPlatform/OpenDJ/actions/runs/39382279910/job/118494172125), `build-maven (ubuntu-latest, 21)`, 1 failure out of 33782: ``` HistoricalCsnOrderingTest.aSessionWhichStartsWhileARecoveryRunsGetsEveryChangeItsReplicationServerMisses:1494 the second round of the recovery searches expected [true] but found [false] ``` It is a race in the test, not in the recovery: - `aChangeBothReplicationServersHold()` ends with `waitUntilTheRecoveryIsOver()`, which returns once the pending changes are out of recovery. - The publisher of that recovery then still runs: it takes its next start, finds none, and as it leaves, `takeStart()` has the broker take the local changes again (`broker.setRecoveryRequired(false)`, `LDAPReplicationDomain` :1045). - `refuseTheLocalChanges()` set the flag with no lock. When the publisher reset it afterwards, the delete of `dnC` and the modify of `dnB`, which the test has the replication server miss, reached it. - The session started again by `disable()`/`enable()` then found a replication server holding every change of this replica. It started no recovery, so the hook on the second search never ran, and the test waited 30 s on `held`. The test log shows exactly that. After `enable()`, the only search of the historical information is the one that loads the state (`ds-sync-hist>=…c135…08`). There is no `publish loop` and no recovery search. The production code is unaffected. Every write of the flag (`:983`, `:1045`, `:7254`, `:7435`) is under `recoveryLock`, and a session which starts between the end of the recovery and `takeStart()` hands its start over to the publisher. Only the test wrote the flag outside that protocol. The same race applies to the other two calls of `refuseTheLocalChanges()` that follow a recovery (the tests that call it right after `aChangeBothReplicationServersHold()`). ### Change `HistoricalCsnOrderingTest.refuseTheLocalChanges()` sets the flag under `recoveryLock`, once `rsUpdater` is `null`. It retries for up to 30 s, the same way `waitUntilAStartIsHandedOver()` reads the publisher. Under that lock, a `null` publisher means that both `takeStart()` and the `finally` of `RSUpdater.run()` have already written the flag, so nothing resets it afterwards. For the tests which refuse the local changes on a fresh domain, no publisher runs and nothing changes. ### Testing Local reactor runs (`-pl opendj-server-legacy -am -Dit.test=…`, JDK 27, macOS): - RED, without the fix: a temporary harness holds the first run of the publisher in `setMissingChangesPublisherRunHook` until the test has called `refuseTheLocalChanges()`, then waits for the publisher to leave. It fails the same way as CI, `the second round of the recovery searches expected [true] but found [false]`, with the same log: no recovery search after `enable()`. A plain sleep in the hook does not reproduce it, because the reset has to land between the refuse and the modify of `dnB`. - GREEN, with the fix and the same harness: `HistoricalCsnOrderingTest` 54/54. - With the fix only (this PR): `HistoricalCsnOrderingTest` 54/54. The harness is not part of the PR.
Fixes #1195, fixes #1197, fixes #1199, fixes #1200, fixes #1201, fixes #1220, fixes #1221, fixes #1223, closes #1203.
The defect
When a directory server connects to a replication server which lacks some of its changes,
LDAPReplicationDomain.RSUpdaterrepublishes them from the historical information of the entries (buildAndPublishMissingChanges()), searching by windows of 10 seconds, while the local changes done meanwhile are only recorded in the domain state (PendingChanges.pushCommittedChanges()) and left to it. Under load it loses changes three ways.In the run of #1195 the one switch of replication server lost 81 changes of cv-c3, not the two deletes the convergence check saw: 21 adds, 2 deletes and 58 modifications. The sequence numbers of that replica in the changelog have 12 gaps, all of them in the window of that recovery and none elsewhere in the run. Later deletes healed the adds, and a check which compares DNs does not see the modifications.
state.cover()). A change committed while the search ran was not in what it found but was covered by that state, so a later change went out in its place and the next round started past it. Deletes escaped only because they are kept aside as they are done.The replication server does not notice: it takes no change of a replica older than the newest one it holds (
LogFile.appendWouldBreakKeyOrdering()), and its server state holds the newest CSN of each replica, somissing-changesreached 0.A publisher which failed also left the pending changes in recovery, so no later local change of the session was published (#1197), and a round whose search failed part-way still published what it had found, which made the changes it missed undeliverable (#1201).
A session which started while the publisher of the previous one ran got no recovery of its own (#1199): the running publisher went on from where it had got to, on the new session, whose replication server may hold less than the one before and then took none of the older changes it missed; or it had already ended its recovery and left without running the one the new session needed. The list of the deletes kept aside, capped at 10000, took what the searches found as well, and dropped its oldest entry - a change a round had just found, or a delete no replication server had - without a trace (#1200); it also kept a local delete which did not go through, and the deletes the data no longer reflected once it was restored or imported (#1221). Rebuilding the local changes of a recovery from the historical information races the commits, cannot publish a change whose values a later change replaced, and holds the local changes back while it searches (#1203). And for as long as a recovery ran, the broker refused every message which was not a recovery one - the acknowledgements of the changes replayed here, the messages of a total update, the monitoring requests - not only the changes of this replica (#1220). A domain which read back a ServerState with no change of its own - the delete of the base entry takes the saved state with it - had its CSN generator go back to the clock, and numbered its next change as, or before, a change of this replica the replication server missed: one of the two was lost (#1223).
The change
PendingChanges.holdBackLocalChangesWhile(), under the lock the pending changes are pushed under) to publish those, and the recovery ends with it: the local operations wait for that publish alone, and no search runs under the hold.PendingChanges.recoveryUntil()switches the pending changes and the broker back together, under that lock.LDAPUpdateMsg.setNotAssured()drops the encoding the message cached.0xffffffff: -1 in a CSN, and the greatest in the text the index ofds-sync-histcompares, so a change in the last millisecond of a window was in neither that window nor the next one. The end carriesInteger.MAX_VALUE, which reads the same in both, and the search listener takes a change numbered exactly the end, as the search filter does.ReplicationBroker.publishRecovery(msg, session)takes the session the recovery was started for (getSession(), read as the session starts), and refuses the message for any other. It returnsfalsewhen the broker refuses the message: it reaches no replication server, its session was replaced, or it was stopped. The round then fails, and nothing it had not published is lost: the deletes kept aside and the local changes the recovery keeps stay where they are. A refusal logs a debug line, notERR_CANNOT_RECOVER_CHANGES.recoveryLock), which is taken inside the connect phase lock of the broker and inside the lock of the pending changes, and under which neither is taken: a recovery is not ended while a start is handed over, and the publisher does not leave before it has run from it.UpdateMsgs only: the changes of this replica, and the announcement that it goes offline. The acknowledgements, the messages of a total update, the monitoring requests and the reset of the generation ID go through.sessionInitiated()then starts a missing change publisher all the same, or hands its start to the one which runs, which finds nothing to publish, or the changes done since, and ends the recovery under the lock of the pending changes. The recovery is not ended insessionInitiated()itself, which runs under the connect phase lock of the broker: a change being pushed waits for that lock holding the lock of the pending changes.ERR_MISSING_CHANGES_NOT_PUBLISHED, waits with the session up (one second per failure in a row, up to ten), lets go of the recovery, and asks for a session restart, which the state checkpointer runs. The recovery of the next session publishes the changes; a session which starts during the wait hands its start over, which ends the wait, and the publisher runs from it with no restart. A publisher which throws an error - which no catch of an exception takes - has the session started again as well. Ending the recovery instead would let the next local change out over the ones it did not publish, which the replication server would then never take.ERR_UNPUBLISHED_DELETE_DROPPEDwith its DN and CSN.PersistentServerStatebrings it up to the changes of this replica the entries hold past the newest one it read. A state which holds none of them is left as it is - that search would go through every entry the replica ever changed - and such a state comes back once the base entry, which holds the saved state, was deleted and added back, and no save wrote into it since: the add of the base entry was then dropped, and the recovery published the deletes of the suffix alone.loadDataState()brings such a state up from the first change the recovery kept. A domain enabled back on its own data loses none of the changes a recovery kept, unless that search does not go through, whichERR_CANNOT_RECOVER_CHANGEStells.CSNGenerator.adjust(null)-loadDataState()passes the newest CSN of this replica in the state it reads, which isnullwhen that state holds none - set the generator to the clock and its sequence number to 0. The session which starts next takes it forward to what its replication server holds only, and the changes that replication server misses are the ones a recovery publishes: the next local change was numbered as one of them, or before, and as a change of another replica it had followed, by as much as that replica's clock runs ahead. An adjust with no CSN leaves the generator where it is.@VisibleForTesting):setMissingChangesSearchHook()hands the search of each round to a test, which makes a change while a round searches, or fails the search;setMissingChangesPublisherRunHook()runs after each run of the publisher, before it takes a start handed over or leaves;setKeptChangesPublishedHook()runs after a round has published the local changes the recovery kept, before it ends the recovery;setMaxDeletesKeptAside()andPendingChanges.setMaxKeptChanges()lower the caps.Not fixed here
Found on the way and filed:
ReplicationBroker.stop()needs the lock of the pending changes before it sets the flag a publish waiting for window credit checks; the rounds which hold the local changes back hold that lock longer.Tests
HistoricalCsnOrderingTest: the recovery publishes to a broker which records what it is handed, or runs in the missing change publisher of a real session, and the changes done after it are looked up in the changelog of a real replication server - which, where local changes go on during the recovery, has to hold them and nothing else. The tests of the search rounds start the recovery after the changes they search for, or run with a recovery which keeps no local change, as one which kept too many; three of them run both ways.CSNGeneratorTest: an adjust with no CSN numbers before neither the CSNs the generator generated nor the ones it received.On f06769e, with the test hooks only, 17 of the 25 new tests of #1195-#1221 are red, each on the part of the change it covers, and two more need a method that head does not have. Six are green there: the fallback past the cap, the load on a recovery which keeps the local changes, the kept changes dropped on a restore, the deletes of a suffix which outlive a state not saved, a kept change published as not assured - parts that head had no way to get wrong, as it kept nothing and dropped no delete - and the wait of round 3. 33 mutants each undo one part of the change, and all of them turn their tests red - two by going round for ever, until the test JVM runs out of heap. The three tests of #1223 are red with the generator of master, which f06769e has: the add of the base entry gets the CSN of the first delete. Three more mutants come with round 5, each red on its test: without the bring-up of a state read back with no change of this replica (f7fcf27 for that test: the replication server gets the deletes of the suffix only), without the check of a start handed over before a held round keeps no more changes, and - in
PersistentServerState- without the bring-up of a state read back to the changes past its newest one. The class: 54 tests, all green;CSNGeneratorTest: 6, all green.org/opends/server/replication/**: not run again locally for this round: the run was cut short as the machine ran out of memory, after other test JVMs on it had taken the ports of its servers; the CI runs it.