[#1061] Ask for the session restart under an owner as well, and leave it to the state checkpointer - #1062
Conversation
d047625 to
25f1034
Compare
|
@maximthomas restacked on master at The conflict was in one place, and it was prose: the javadoc of Re-run after the restack, one JVM per class: |
25f1034 to
529afd1
Compare
|
@maximthomas rebased onto master at The conflict was the javadoc of On this head: Not measured again: the four mutants of the table. The three arms and the parked road's gate are the lines they were measured on, unchanged by the rebase. |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The fix sits where #1061 is: every road which releases a change now asks for the restart before it reads the owner, so a total update which never begins leaves the request to the state checkpointer.
- The request moves above
sessionHasAnOwner()in all three roads,recoverFromReplayFailure()(:4004),giveBackParkedChanges()(:4234) andabandonReplay()(:4278). The owner arms stay silent and uncounted, and the three roads follow one policy. - Each new
ReplayDuringImportTestcase sends its change through the replication server and takes the watchdog road,abortStalledInitializeFromRemote(0). The description reports each arm's revert mutant red on its own case.
issue (non-blocking): abandonReplay() asks for a restart when its change is no longer listed, so a request made after an owner's clear outlives that owner.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:4259-4285, :5942-5946, :5428
replayFailed(csn) does nothing for an unlisted CSN, and request(NOW) now comes before the owner read. Two owners clear the requests while they still own the session:
- The import's
finallyclears them, andimportInProgress()holds untilReplicationDomain.initialize()runsbroker.reStart(false)and thenreleaseIEContext(). enable()clears them, then runsloadDataState(), and only then setsdisabled = false.
A thread can read the flag as set and reach abandonReplay() only after that clear: after processUpdateDone()'s ack publish, or as a straggler past the bounded awaitReplayDrained(). Its NOW request survives, and the checkpointer then stops and restarts the session the owner has just started, for a change the clear already dropped. Nothing is lost: it is one extra bounce, and the reloaded state absorbs the redelivery. At the base the owner arm returned before asking. The same window contradicts enable()'s "every request standing here is that one". recoverFromReplayFailure() does not hit it, because recordReplayFailure() returns null for an unlisted CSN.
// RemotePendingChanges
public boolean replayFailed(CSN csn)
{
pendingChangesWriteLock.lock();
try
{
final PendingChange change = pendingChanges.get(csn);
if (change != null && !change.isCommitted() && change.isOwnedBy(Thread.currentThread()))
{
change.setOwner(null);
changeBeingReplayed.remove(Thread.currentThread(), csn);
return true;
}
return false;
}
finally
{
pendingChangesWriteLock.unlock();
}
}
// LDAPReplicationDomain.abandonReplay(CSN)
if (!remotePendingChanges.replayFailed(csn))
{
// No longer listed: the owner which cleared the pending changes starts the session again
// from the state it reloaded, which asks for everything that state does not cover.
return;
}
sessionRestarts.request(SessionRestart.NOW);Or: keep the code as is and say in the enable() and import-finally comments that this window exists.
suggestion (non-blocking): The abandon case never checks that the change took the uncounted abandon road, and it drops the result of markInProgress().
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:509, :496-528
The javadoc says the change is abandoned "without being counted against its budget", but the case reads no failure record, budget or counter. Two mutants keep it green:
recordReplayFailure(csn, now)added insideabandonReplay().- A stopping thread's change sent to
recoverFromReplayFailure()under an owner only. That road also asks NOW for a stopping thread and logs nothing under the owner.
replayed-updates-failed counts only give-ups, so it cannot tell the two roads apart. The redelivery half of the case is pinned.
assertTrue(domain.markInProgress(delivered), "the delivery was not handed to this thread");
domain.replay(delivered, new AtomicBoolean(true));Pin: after the replay, assert that no replay failure is recorded for csn, reading RemotePendingChanges the way RemotePendingChangesTest does. Or drop the "not counted" half from the javadoc.
suggestion (non-blocking): Nothing can kill the removal of the last-resort gate in replay(), which the description calls load-bearing.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:2910-2913
The gate is reached only from catch (Throwable recoveryFailure). Under an owner the recovery has no throw a test can reach: the three arms ask and return, and failSessionRestartIfATestAskedFor() throws only inside restartSession(), after its owner check. So every test which enters this catch has no owner, for example ParkedChangeGiveBackTest, UpdateOperationTest and SessionRestartBackoffTest. The mutant below survives. The gate is unchanged from the base apart from its comment, and the mutant table pins only the parked-road gate at :2826.
// in place of: if (!sessionHasAnOwner()) { runRequestedSessionRestarts(); }
runRequestedSessionRestarts();Pin: add a hook which throws an Error out of the recovery, for example out of giveBackParkedChanges(), while the exporter holds the InitializeRequestMsg. Then call abortStalledInitializeFromRemote(0) and awaitDelivery(csn, …), as the parked case does. The mutant spends the request on the refusal, and nothing is delivered again.
suggestion (non-blocking): The isConnected() checks right after the in-thread replay() cannot see a restart that ran on the test's own thread.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:470, :517
A restart run on that thread goes through restartSession() inline: disableService(), the backoff, enableService(), then the broker's connect. So the session is back up before the read. The message says the check catches a session stopped under the owner, but it passes either way. The neighbouring WARN/NOTE checks and the redelivery wait kill every single mutant. A double mutant survives: the owner arm calls restartSession(true) itself and the refusal at :4304 is removed. The :517 read, which follows a NOW restart with no backoff, may also race the replication server unregistering the replica (not traced).
assertTrue(domain.isConnected(), "the session is not up after the replay under the owner");Pin: to say that no restart ran under the owner, assert that a restart count or the broker's session identity is unchanged across the replay() call.
nitpick (non-blocking): The import finally still says its clear is for a restart asked for "before the total update owned the session".
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:5935-5939
With this PR, every give-up under the import owner asks before it returns. So the requests this clear forgets are mostly ones made while the import owned the session, and the new sessionHasAnOwner() javadoc ("the import at its end forget[s] the request") relies on that.
* listed a change meanwhile: the listener thread is the one running this import,
* and the replay threads gave up every attempt while the flag was set. The restarts
* they asked for on their way out, while the total update owned the session, and
* any asked for before it did, go with them, as do the deliveries folded into no
* warning: the caller starts the session again from the reloaded state.…r as well, and leave it to the state checkpointer A change released while the session of its domain had an owner - the domain shutting down or disabled, or a total update into this replica from the moment it is asked for - was released and nothing more: recoverFromReplayFailure(), abandonReplay() and, with OpenIdentityPlatform#954, giveBackParkedChanges() all returned before asking for the session restart. Two of the three owners forget the pending changes and every request with them, disable() with enable() and the import at its end, so nothing was lost there. The third does not: a total update which is asked for and never begins - initializeFromRemote() refused at publish, an ErrorMsg from the exporter, the watchdog of InitializeTask, or the second attempt initialize() makes after a failed import - ends in releaseIEContext() alone, and a change released under it stayed listed, uncommitted and owned by nobody until the next failed replay of this domain restarted the session. On a domain which then went quiet that was for good: a commit moves the ServerState no further than the oldest uncommitted change, so the state in memory, and the one persisted from it, stopped at the change - the shape of OpenIdentityPlatform#954, one road over. OpenIdentityPlatform#968 left it that way because at the time the request was a flag only the thread of a failed replay ran: a flag set under an owner was served by the next failed replay, which would have set it itself. Since OpenIdentityPlatform#981 the state checkpointer runs the request nobody ran, and holds it for as long as the session has an owner - which the last resort of replay() relies on already: its request is made under an owner too, and only its run is gated. The three roads now ask on every road, as one policy: the request is made once the change is released, before the owner is read, and the owner arm returns with it standing. The owners which forget the pending changes forget the request with them; the one which does not leaves it to the checkpointer, which runs it within its tick of the owner letting go and has the replication server send the change again. The lines which say the change is being asked for again, and the count of the deliveries given back, stay off the owner roads: a server which is shutting down abandons every change in flight, and none of them is delivered again before it is started back. The runs of replay() - the parked road and the last resort - keep their gates: since OpenIdentityPlatform#1049 runRequestedSessionRestarts() leaves every request standing while a total update is processed, so the total update which never begins cannot have its request spent on a refusal there, and the owners a run would still be refused under - the domain disabled or shutting down - forget the request with the pending changes. Pinned in ReplayDuringImportTest on the one owner a test can hold open and then take away: the total update is asked for, the exporter holds the request, and in the place of the answer the request is given up through abortStalledInitializeFromRemote(0). One case per road: a change whose attempts in place are spent under the request, one a stopping thread abandons under it, and one a replay which is unwound had parked - behind a change another thread of the test holds before its operation is built, so that nothing has failed and the give-back's request is the only one standing. Each change travels the replication server, and each is delivered again over the session the checkpointer restarts once the request is given up, replayed and covered.
…rgot, and count the restarts run under the owner Review of 529afd1. abandonReplay() asked for the session restart whether or not the change was still listed. A replay thread which read the flag while an owner held the session and reached the give-up only after the owner forgot the pending changes - the import at its end, or enable() - left a NOW request standing past that clear, and the state checkpointer stopped and started the session the owner had just started, for a change which was gone. RemotePendingChanges.replayFailed() now says whether it released the change, and abandonReplay() asks for nothing when it did not - the way recoverFromReplayFailure() already asks for nothing when recordReplayFailure() finds no change. The other road to false, a change another thread took over, needs no restart either. The comment on the import's clear says the requests it forgets are mostly ones made while the import owned the session. ReplayDuringImportTest: the new case abandons a change the import forgot and finds no restart run within two ticks of the checkpointer. The abandon case asserts that the delivery was handed to the thread and that nothing was counted against the change's budget (changes-with-failed-replay). The three owner cases count the restarts run on the thread of the test with failNextSessionRestarts(1) in place of isConnected(), which a restart run inline is back up before. RemotePendingChangesTest pins what replayFailed() returns: released by its owner, not by another thread, not twice, not once forgotten or committed.
529afd1 to
ed5aad5
Compare
|
@maximthomas thank you. Two things in this push, head The rebase over #1049. #1049 made
The 1. 2. The abandon case does not pin "not counted": taken. 3. The last-resort gate: not pinned, because after #1049 the mutant is equivalent. 4. 5. The comment on the import's clear: taken, in your words, plus one sentence: a replay thread that reaches its give-up after this clear finds its change unlisted and asks for nothing. Run on the round's head, one JVM per class: |
There was a problem hiding this comment.
praise: The round commit closes the window from round 1 with a return type and no new state.
RemotePendingChanges.replayFailed()now returns whether it released the change.abandonReplay()(LDAPReplicationDomain.java:4360) asks for nothing when it did not, andRemotePendingChangesTestpins every arm of that result.- The owner cases count restarts with
failNextSessionRestarts(1)/getSessionRestartFailuresLeft(). This sees a restart run inline, whichisConnected()could not. - The rebase over #1049 keeps the owner roads quiet:
giveBackParkedChanges()returnsCollections.emptyList()under an owner, so the caller has nothing to run or report.
Fixes #1061.
On master. Rebased onto master at
e333af0c8f:d23b4fcd53is the[#1061]commit, anded5aad5bf0is the round commit for the review of 24.09. #1049 ([#1048]) landed over the same three roads, and the PR was conflicting. It maderunRequestedSessionRestarts()leave every request standing whileieRunning(), madegiveBackParkedChanges()return the changes it handed back, and addedNOTE_REPLAY_SESSION_RESTART_HELD_BY_TOTAL_UPDATE. InrecoverFromReplayFailure()the request comes first, then the owner arm returns, then #1049'swarned/ held-restart note.giveBackParkedChanges()asks before the owner read and returnsCollections.emptyList()under the owner, so the caller has nothing to run or report. Earlier, #1042 ([#1040]) rewrote the javadoc ofsessionHasAnOwner(). Its paragraph on the configuration change (restartService()) stands after this PR's paragraph on the three roads: they are two roads, andrestartService()records the suppressed restart for the administrator whilerestartSession()leaves the request to the checkpointer. The third road,giveBackParkedChanges(), is #954's, on master since the squash of #988.What was wrong
recoverFromReplayFailure(),abandonReplay()and - with #954 -giveBackParkedChanges()release the change they hand back and, while the session has an owner (sessionHasAnOwner(): the domain shutting down or disabled, or a total update into this replica from the moment it is asked for), asked for no session restart. Two of the three owners forget the pending changes and every request with them -disable()/enable()and the import'sfinallyclear both - so nothing was lost there. The third does not: a total update which is asked for and never begins -initializeFromRemote()refused at publish, anErrorMsgfrom the exporter, the two-minute watchdog ofInitializeTask(abortStalledInitializeFromRemote()), or the second attemptinitialize()makes after a failed import, which re-publishes the request and keeps the context - ends inreleaseIEContext()alone, and a change released under it stayed listed, uncommitted and owned by nobody until the next failed replay of this domain restarted the session. On a domain which then goes quiet that is for good:commit()moves the ServerState no further than the oldest uncommitted change, so the state in memory, and the one persisted from it, stop at the change - the shape of #954, one road over.#968 left it that way on purpose: the request was a flag which only the thread of a failed replay ran, so a flag set under an owner was served by the next failed replay, which would have set it itself. #981 changed that premise - the state checkpointer runs a request nobody ran, and holds it for as long as
ieRunning()- and the last resort ofreplay()relies on it already: its request is made under an owner as well, and only its run is gated. Found in the fourth review of #985, filed as #1061.What changed
remotePendingChanges.replayFailed(csn)/releaseParkedChangesOwnedByCurrentThread(), thensessionRestarts.request(…), thensessionHasAnOwner()- and the owner arm returns with it standing. What is asked for is what was asked for before:NOWon a thread which is stopping or which anOutOfMemoryErroris ending,AFTER_BACKOFFotherwise;abandonReplay()asksNOWas it did. The owners which forget the pending changes forget the request with them (disable():5248,enable():5428, the import'sfinally:5943); the one which does not leaves it to the checkpointer, which runs it within its tick ofreleaseIEContext()and has the replication server send the change again.WARN_REPLAY_RETRYING_CHANGE(through the throttle of Replication: the replay retry warning is logged once per delivery, with nothing bounding it once the give-up budget is raised #942),NOTE_REPLAY_ABANDONED_CHANGEandNOTE_REPLAY_PARKED_CHANGE_GIVEN_BACKare not logged under an owner, and the parked deliveries are not counted as processed, as before: a server which is shutting down abandons every change in flight, and none of them is delivered again before it is started back, so a line which says the replication server sends the change again would not hold on every owner road. On the never-begun road the change is now asked for again without a line saying so, and a parked delivery given back there is counted by the delivery which replays it; said here rather than papered over with a line of its own.replay()stay, as an early exit. They are the parked road and the last resort. They were load-bearing when this PR was written, and are not since [#1048] Hold the session restart a released change asks for while a total update runs #1049: under a total update into this replicarunRequestedSessionRestarts()leaves the request standing by itself, becauseimportInProgress()impliesieRunning(). The owners it would still be refused under - the domain disabled or shutting down - forget the request with the pending changes. No test can tell a gate from its removal, so neither is pinned. The javadoc ofsessionHasAnOwner(), the comments of the three arms, of the parked road, ofenable()(whose account of thedisabled = falseordering rested onabandonReplay()asking for nothing) and of the test hookrequestSessionRestart()say what holds now.RemotePendingChanges.replayFailed()returns whether it released the change, andabandonReplay()makes no request when it did not. A replay thread which reaches its give-up after the import's clear, orenable()'s, would otherwise leave a NOW request standing past that clear, and the checkpointer would bounce the session the owner had just started.recoverFromReplayFailure()already behaved this way throughrecordReplayFailure().What this does not close. A no-owner thread which read no owner, made its request and reached
runRequestedSessionRestarts()while the owner arrived spends the merged flag - its own and any request made under the owner in between - onrestartSession()'s refusal. That window is the few statements between the read and the lock, the same class as the last resort's today, and it is not a regression (before this, nothing was asked for under the owner at all); closing it for good meansrestartSession()saying it refused and the caller giving the request back, which is a change to the loop ofrunRequestedSessionRestarts()and belongs to a follow-up. #1049 closes the import direction of it in passing - itsrunRequestedSessionRestarts()leaves the requests standing whileieRunning()- and #1045 turns the owner read ofrestartSession()into a claim. With #1049 in, the import part of the parked road's gate is no longer load-bearing, and the mutant of the table below which pinned it survives by construction.Tests
All in
ReplayDuringImportTest, on the one owner a test can hold open and then take away:initializeFromRemote()with the exporter holding theInitializeRequestMsg, and in the place of the answerabortStalledInitializeFromRemote(0)- the watchdog's road, and the one road out of an unanswered request a test can take at a time of its choosing (anErrorMsgreleases the context only through the task, which the test does not have; production always has one). Each change travels the replication server - published by the exporter's broker, taken off the domain's synchronous queue by the test - which is what has it to deliver again; a message handed to the domain, as the class's other cases do, is one the replication server never had.aChangeReleasedUnderARequestWhichIsNeverAnsweredIsDeliveredAgain(new)recoverFromReplayFailure()- the attempts in place spent on an entryUUID search which does not run!cover(csn); after the abort the change is delivered again within the checkpointer's tick + the 1 s backoff, replayed, applied, coveredaChangeAbandonedUnderARequestWhichIsNeverAnsweredIsDeliveredAgain(new)abandonReplay()- replayed with the stopping flag set, abandoned at the top of its first attemptaParkedChangeGivenBackUnderARequestWhichIsNeverAnsweredIsDeliveredAgain(new)giveBackParkedChanges()- the parked change waits for one another thread of the test holds before its operation is built (ModifyMsgWhoseOperationWaitsToBeBuilt, new), so nothing has failed and the give-back's request is the only one standing; the replay unwound is the OOME-on-ack fixture of #954dependent-changes-size1 → 0, held change committed when let go, parked change delivered again after the abort and coveredaParkedChangeGivenBackWhileTheRequestIsOnItsWayLeavesTheSessionToTheOwner(renamed from…IsNotAskedForAgain, assertions unchanged)aChangeAbandonedOnceTheImportForgotItAsksForNoRestart(new, round of 24.09)abandonReplay()after the import forgot the change: the test thread owns a delivery across a whole import, then abandons itfailNextSessionRestarts(1)unspent), no NOTE 309The three new cases were run first against the class as it stands on #985's head (
git show HEAD:…LDAPReplicationDomain.javacompiled ahead oftarget/classes): 3/3 red, each on the wait for the redelivery - "was not delivered again once the request gave up … within 30000 ms".Mutants, each compiled in place of the class and run against the case meant to catch it:
recoverFromReplayFailure()asks only when the session has no owner (the request moved back below the owner arm)abandonReplay()asks only when the session has no ownergiveBackParkedChanges()asks only when the session has no owner!shutdown.get() && !disabledin place of!sessionHasAnOwner()runRequestedSessionRestarts()holds the request itself whileieRunning()abandonReplay()ignores whatreplayFailed()returnsaChangeAbandonedOnceTheImportForgotItAsksForNoRestart: "expected [1] but found [0]"recordReplayFailure(csn, now)insideabandonReplay()abandonReplay()callsrestartSession(false), andrestartSession()stops refusingThe three owner cases count the restarts run on the thread of the test with
failNextSessionRestarts(1)/getSessionRestartFailuresLeft(). They used to readisConnected(), but a restart run inline is back up before that read. The abandoned case also asserts that the delivery was handed to the thread and thatchanges-with-failed-replayis 0.RemotePendingChangesTestpins whatreplayFailed()returns.Run on
ed5aad5bf0, one JVM per class:ReplayDuringImportTest13/13,RemotePendingChangesTest25/25,ParkedChangeGiveBackTest4/4,ReplayDuringExportTest2/2,DisabledDomainServerStateTest2/2,SessionRestartBackoffTest3/3,UpdateOperationTest41/41,Skipped: 0. Ond23b4fcd53alsoConfigChangeDuringImportTest4/4 andSessionRestartRequestsTest7/7. Before the rebases, on25f1034981:ReplayDuringImportTest9/9,ParkedChangeGiveBackTest3/3 (as of #985's fifth round),SessionRestartBackoffTest3/3,SessionRestartTest2/2,DisabledDomainServerStateTest2/2,RemotePendingChangesTest24/24,SessionRestartRequestsTest7/7,ServerStateFlushTest4/4,LDAPReplicationDomainConfigChangeTest9/9,DependencyTest3/3,UpdateOperationTest40/40,InitOnLineTest10/10,GenerationIdTest4/4,ReSyncTest2/2,NamingConflictTest21/21,AssuredReplicationPluginTest14/14,ReplicationDomainTest12/12 - the classes which restart a session, drive a total update or read the requests, 169 tests,Skipped: 0.