[#1041] Claim the import context for the length of a session restart, and refuse the total update which lands across it - #1045
Conversation
89f2b1d to
8287944
Compare
|
@maximthomas rebased on Two things changed on the way, both in
|
8287944 to
972bd1b
Compare
|
@maximthomas rebased onto master at
|
972bd1b to
eccfe94
Compare
|
@maximthomas rebased onto master at The conflict was in Re-run after the rebase: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The claim sits where the race is, and the release fix beside it closes a leak older than this PR.
ReplicationDomain.disableServiceUnlessImportInProgress()(:3502) claims the reference underserviceStateLock, the one lockrestartSession()already holds, and releases it in afinallyonce the listener thread is joined — the one thread which claims a total update this replica did not ask for.releaseIEContext(ImportExportContext)(:2089) as a CAS on the given context, withinitializeFromRemote()releasing only what it acquired (:2517-2520): at the base adsreplication initializerefused against a live import or export wiped that operation's context.getImportExportContext()(:4022-4025) as the single reader keeps the sentinel out ofieRunning(),importInProgress()and the monitor, so the checkpointer goes on saving across a restart.
issue (non-blocking): The post-claim re-read narrows the stop roads which make no claim; the description and the javadoc say it closes them.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2639-2650, :3496-3498, :3514-3519
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:5683-5684
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java:2722-2745
if (broker.shuttingDown()) at :2639 is a check-then-act: nothing orders it against broker.stop() on the roads which call disableService() bare — disable() (LRD :5050), shutdown() (LRD :2668), restartService() (:3659), the config roads (LRD :1031, :4157), and the export arm of disableServiceUnlessImportInProgress() itself (:3514-3519: the CAS lost to an export, the sentinel never set). The listener cannot take serviceStateLock (the join inside disableService() would wait for it). A stop landing after the re-read runs the total update over a stopped broker: the signalStatusChange() NPE if before setNewStatus, else importLDIF over an empty stream — the first shape of #1041. The window is not a few statements: importBackend() runs importingData = true; awaitReplayDrained(); (LRD :5683-5684, bounded by replayDrainTimeoutInMs) and preBackendImport() before the first broker.receive() (:2160-2171). On the export arm the restart is still before shutdown = true — broker.stop() publishes the ReplicaOfflineMsg first (RB :2722-2745) — when the export releases and the listener claims and reads false. The body says "That closes what the claim does not reach" and "the re-read keeps a disable() or a shutdown from being it before the first entry"; the javadoc at :3496-3498 says the export-then-stop total update "is refused by the listener on the broker it finds stopping". Both hold only when shutdown = true precedes the re-read. Every one of these roads had the same window at the base, so this is not a regression; if the residual is left to #1039 deliberately, the fix is the wording alone.
* not an owner: the session is stopped from under it and the exporter reports the cut, as
* it does for every other stop. A total update which lands between the end of that export
* and the stop is refused by the listener when it reads the broker as stopping after its
* claim; a stop which lands after that read and before the first entry is received still
* ends it as an import over nothing (issue #1039) - the read narrows that window, it does
* not close it.Or: close the export arm the way the sentinel closes the no-owner arm — a second flag with each side writing before it reads (the restart sets sessionStopping before it reads the context, the listener reads sessionStopping after its CAS; the worst case is a spurious refusal). disable()/shutdown() stay #1039's: closing them means deciding what a stop does to a running import.
issue (non-blocking): ERR_INIT_REJECTED_SESSION_STOPPING_330 describes an import into this server, and is thrown for an export out of it as well.
opendj-server-legacy/src/messages/org/opends/messages/replication.properties:704
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2072-2074, :1645
acquireIEContext(boolean importInProgress) picks the text on importExportContext.get() == SESSION_BEING_STOPPED alone; initializeRemote() acquires through the same CAS at :1645 with false. An export started while a restart holds the sentinel — an InitializeRequestMsg taken off the session just before the stop, or a local InitializeTargetTask for the length of disableService() — is logged at :1667 and reported to the requester or in the task result as "The total update of domain "%s" into this directory server was refused: ... the entries would have been read over that session". The refusal is right; nothing branches on the text; the task result describes an import nobody asked for.
final LocalizableMessage message = importExportContext.get() != SESSION_BEING_STOPPED
? ERR_SIMULTANEOUS_IMPORT_EXPORT_REJECTED.get()
: importInProgress
? ERR_INIT_REJECTED_SESSION_STOPPING.get(getBaseDN())
: ERR_EXPORT_REJECTED_SESSION_STOPPING.get(getBaseDN());Or: one direction-neutral text — "The total update of domain "%s" was refused by this directory server: its session to the replication server is being stopped, and the entries would have been streamed over that session."
suggestion (non-blocking): The "release only your own context" fix has no red test.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2089, :2517-2520
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:493
No test calls initializeFromRemote() against an owner or against the claim: ReplayDuringImportTest:268 and ReplicationDomainTest acquire on a free context, InitOnLineTest.initializeSimultaneous is enabled = false, ReplicationDomainTest:520-541 refuses initializeRemote, whose catch never released. Each single mutant is equivalent by construction (with the guard every caller passes its own context; without it compareAndSet(null, null) is a no-op); the double mutant — the catch back to an unconditional importExportContext.set(null) — restores the base's leak and every case stays green.
assertTrue(domain.isConnected(), "the session was stopped before the stop was held");
assertThatThrownBy(() -> domain.initializeFromRemote(EXPORTER_ID, null))
.as("a total update asked for while the session is being stopped was not refused")
.isInstanceOf(DirectoryException.class)
.hasMessageContaining(ERR_INIT_REJECTED_SESSION_STOPPING.get(baseDN).toString());Pin: in aRestartDecidedBeforeTheImportIsClaimedRefusesTheImport, while the stop is held; the case then goes on as it stands — under the double mutant the refused request wipes the claim, the listener's CAS wins and the NOTE_FULL_UPDATE_ENGAGED_FROM_REMOTE_START count at :503 goes red.
suggestion (non-blocking): The export arm of disableServiceUnlessImportInProgress() is unpinned.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:3514-3519
The arm is observable only as a replay-driven restart under an export out of this replica. The classes with an export (GenerationIdTest, InitOnLineTest, FractionalReplicationTest, ProtocolCompatibilityTest, SynchronizationMsgTest, ReplicationDomainTest) and the classes with a failed-replay restart fixture (UpdateOperationTest, AssuredReplicationPluginTest, IsServerFailureTest, NamingConflictTest, ReplayDuringImportTest, SessionRestart*Test) are disjoint; the mutant if (owner.importInProgress()) -> if (owner != null) — any owner leaves the session alone — is green on every case.
// on the ReplayDuringImportTest fixture: the domain exports into the exporter broker, which
// reads nothing, so the export holds the context; then a replay fails the way test 1 does
final AtomicReference<Throwable> exportFailure = new AtomicReference<>();
final Thread export = new Thread(() -> {
try { domain.initializeRemote(EXPORTER_ID, null); } catch (Throwable t) { exportFailure.set(t); }
}, "export");
export.start();
waitUntil(domain::ieRunning, "the export did not claim the context");
// ShortCircuitPlugin on SEARCH + replayMsg(...) as in aRestartDecidedBeforeTheImportIsClaimedRefusesTheImport
waitUntil(() -> !domain.isConnected(), "the restart left the session to the export");
waitUntil(domain::isConnected, "the session was not started back after the restart");Pin: under the mutant the restart returns false and the session never drops — the first waitUntil goes red.
suggestion (non-blocking): The release of a freshly claimed import on the re-read arm is unpinned.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2648
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:593
aDomainDisabledBeforeTheImportIsClaimedRefusesTheImport asserts the entry, the START count, the listener deaths and the refusal count; none reads the context after enable(). Delete releaseIEContext(ieCtx); at :2648 and the import context stays claimed for the domain's life — ieRunning() true, the checkpointer skips every ds-sync-state save (LRD :723) silently, every later total update refused as simultaneous — and the case is green.
waitUntil(domain::isConnected, "the session was not started back by enable()");
assertFalse(domain.ieRunning(), "the refused import left its context claimed");suggestion (non-blocking): The release of SESSION_BEING_STOPPED once the stop is done is unpinned.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:3527
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:525-530
Nothing in aRestartDecidedBeforeTheImportIsClaimedRefusesTheImport claims a total update after the restart. Delete the finally's CAS at :3527 and the sentinel stays: getImportExportContext() hides it from every reader, a later restart's loop reads importInProgress() false and stops as before, and every later acquireIEContext — a dsreplication initialize into this replica, a remote InitializeTargetMsg, an export — is refused with 330 until the domain object goes. The case is green.
waitUntil(domain::isConnected, "the session was not started back after the restart");
// The claim of the stop was released: the next total update into this replica runs.
startImportInto(exportedEntries().length);
finishImport(exportedEntries());
assertThat(errorLogRecordsOf(NOTE_FULL_UPDATE_ENGAGED_FROM_REMOTE_END.ordinal()))
.as("the total update after the restart did not run to its end")
.hasSize(totalUpdatesEndedBefore + 2);Pin: under the mutant the second total update is refused with 330 and startImportInto times out on the backend never being deregistered. The existing hasSize(totalUpdatesEndedBefore) at :530 moves before the second import.
suggestion (non-blocking): The sentinel filter in getImportExportContext() is unpinned.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:4022-4025
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:493
The sentinel is held only inside restartSession(); test 1 holds the stop at :486-493 and asserts only domain.isConnected() there, test 2 holds nothing across a claim, and finishImport polls ieRunning() after a millisecond stop is over. The mutant return ieCtx; — the claim visible to ieRunning(), the checkpointer's save at LRD :723 and the self-initiated arm of initialize() — is green on every case.
assertTrue(domain.isConnected(), "the session was stopped before the stop was held");
assertFalse(domain.ieRunning(), "the claim of the stop is visible as a running import");Pin: the hook runs after the CAS at :3506, inside disableService(); the line is red under the mutant.
nitpick (non-blocking): The refusal is logged at ERROR; the sibling refusal of a remote's export request is logged at NOTICE.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2567, :1667
rejectInitializeTarget() does logger.error(reason) on both arms — including the re-read after a routine disable() or a server shutdown, and the pre-existing simultaneous-import arm, which logged nothing locally at the base — while :1667, this server refusing a remote's export request on the same detail, is logger.info(NOTE_FULL_UPDATE_REMOTE_REQUEST_REJECTED). An operator who disables a domain with an InitializeTargetMsg in flight gets an ERROR line on the replica which did nothing wrong. The tests match msgID= in the error log and count NOTE records through the same helper, so logger.info with a NOTE-category message keeps them green. Your call if the ERROR is wanted.
eccfe94 to
7f9aded
Compare
|
@maximthomas round 2 on issue: the post-claim re-read narrows the stop roads which make no claim; the description and the javadoc say it closes them. Taken, as the wording. * not an owner: the session is stopped from under it and the exporter reports the cut, as
* it does for every other stop. A total update which lands between the end of that export
* and the stop is refused by the listener when it reads the broker as stopping after its
* claim; a stop which lands after that read still has the import run over a session which
* is going down, and end as a failed import over the suffix it has replaced (issue
* #1039): the read narrows that window, it does not close it.The second flag is not taken. It would close the export arm the way the sentinel closes the no-owner arm and leave issue: Taken, as the one direction-neutral text. ERR_INIT_REJECTED_SESSION_STOPPING_330=The total update of domain "%s" was refused by this \
directory server: its session to the replication server is being stopped, and the entries \
would have been streamed over that session. Ask for the total update again once the session \
is backOne text rather than two: nothing branches on it, the key is direction-neutral already, and an ordinal in a file the open PRs are appending to is worth not taking twice. The comment at suggestion: the "release only your own context" fix has no red test. Taken, where you put it - in assertThatThrownBy(() -> domain.initializeFromRemote(EXPORTER_ID, null))
.as("a total update asked for while the session is being stopped was not refused")
.isInstanceOf(DirectoryException.class)
.hasMessageContaining(ERR_INIT_REJECTED_SESSION_STOPPING.get(baseDN).toString());Under the double mutant - the CAS back to It also moved where the no-claim mutant is caught: with suggestion: the export arm of Taken, as a case of its own: assertTrue(stopHeld.await(30, TimeUnit.SECONDS),
"the restart left the session to the export: an export is not the owner a total"
+ " update into this replica is");Under suggestion: the release of a freshly claimed import on the re-read arm is unpinned. Taken, the line as you wrote it, in waitUntil(domain::isConnected, "the session was not started back by enable()");
assertFalse(domain.ieRunning(), "the refused import left its context claimed");suggestion: the release of Taken, as the second total update at the end of case 1, with the existing count moved ahead of it. startImportInto(exportedEntries().length);
finishImport(exportedEntries());
assertThat(errorLogRecordsOf(NOTE_FULL_UPDATE_ENGAGED_FROM_REMOTE_END.ordinal()))
.as("the total update after the restart did not run to its end")
.hasSize(totalUpdatesEndedBefore + 2);With the suggestion: the sentinel filter in Taken, the line as you wrote it, where the stop is held and the claim is the only thing in the reference. assertFalse(domain.ieRunning(), "the claim of the stop is visible as a running import");nitpick: the refusal is logged at ERROR; the sibling refusal of a remote's export request is logged at NOTICE. Kept at ERROR, and this is the one thing I am not taking on its own terms - say the word and it goes to The two are not the same event. The Runs on this head, class = JVM:
Each road out of the claim, one mutant each, all on
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: Round 2 turns every round-1 gift into a line that fails, and the new export case checks the decision itself, not a side effect of it.
aRestartDecidedBeforeTheImportIsClaimedRefusesTheImportnow asserts, while the stop is held,assertFalse(domain.ieRunning())(:607) and thatinitializeFromRemote()is refused with 330 (:612-616). The claim is visible, and the failed acquire does not release it.aRestartDecidedWhileAnExportRunsStopsTheSessionItStreamsOverwaits on the stop seam (:822), so the case reads theowner.importInProgress()decision indisableServiceUnlessImportInProgress()directly.- The javadoc and the comment at the re-read now say it narrows the non-claiming stop roads and does not close them. That is accurate on a base that has #1044.
suggestion (non-blocking): The post-claim re-read of the broker is only on the remote-initiated arm of initialize(). A total update this replica asked for itself still runs over a session that is stopping.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2600-2618, :2644
The self-initiated arm (initiator == getServerId()) takes the context initializeFromRemote() acquired, calls markInitStartReceived() and goes straight into importBackend(), with no broker.shuttingDown() read. Nothing releases that pending context on the disable() or domain-shutdown roads: disableService() only stops the broker and joins the listener. If the stop lands across the answering InitializeTargetMsg, the import runs over the stopped broker and ends as ERR 329 over the suffix it has replaced (#1039). The remote arm now refuses that same interleaving at :2644. This gap was already there at the base, and the restart road cannot reach it (importInProgress() is true, so the session is left alone). The fix is either the same read, or one sentence in the javadoc saying this arm is left as it was.
if (ieCtx == null || !ieCtx.markInitStartReceived())
{
...
return;
}
if (broker.shuttingDown())
{
ieCtx.setExceptionIfNoneSet(new DirectoryException(ResultCode.OTHER,
ERR_INIT_REJECTED_SESSION_STOPPING.get(getBaseDN())));
completeInitializeTask(ieCtx);
return;
}question (non-blocking): Once #1049 is in, which contract stands: does a restart cut an export (this PR), or wait for it (#1049)?
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:822
This PR pins that a replay restart reaches the stop under an export ("an export is not an owner"). #1049 (head 2506e2a8) makes runRequestedSessionRestarts() start with if (ieRunning()) { return false; }, and that method is the only replay-road caller of restartSession(). With both merged, the restart waits out the export, the stop seam never runs, and stopHeld.await(30, TimeUnit.SECONDS) fails. The two changes touch different methods, so whichever one is rebased second will rebase cleanly and still go red. This does not block this PR, because its contract is consistent at this head. The PR merged second has to drop or invert this case, or narrow the hold to imports. Not built or run on the combined head.
suggestion (non-blocking): The new refused-request pin is red only when both defences are reverted. The CAS in releaseIEContext() is not pinned on its own.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:607-616
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2090-2093, :2139, :2557
If the if (ieCtx != null) guard in initializeFromRemote() stays, the refused road never calls releaseIEContext(), so a body put back to importExportContext.set(null) keeps the case green. In round 1 I called that single mutant equivalent. It is not: completeInitializeTask() is reached from the ErrorMsg arm (:2139) and from the stalled watchdog (:2557), and each holds its own read of the same context. Under set(null), the second release clears whatever was claimed in between (the sentinel, or a new import). Under the CAS it does nothing. No case drives two completions. Pinning it needs a seam between the watchdog's read and its release. The cheaper option is to make the comment claim only what the case pins:
/*
* A total update asked for here is refused against the claim of the stop, and the
* claim is left where it is: the road which fails to acquire a context of its own
* releases nothing.
*/suggestion (non-blocking): The readers moved to getImportExportContext() are pinned only through ieRunning(). The self-initiated arm of initialize() could read the sentinel again and no test would notice.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2602
assertFalse(domain.ieRunning()) at :607 pins the getter, not the call sites that use it. If :2602 goes back to importExportContext.get(), it gets SESSION_BEING_STOPPED. markInitStartReceived() returns true on it (abandonedAsStalled is false), and an import then runs on the static sentinel that every domain shares. This is reachable when the #861 watchdog has abandoned the request and a restart claims before the stale InitializeTargetMsg arrives. The only case that holds the claim sends a non-self InitializeTargetMsg, so every case stays green. Not run.
Pin: while the stop is held in aRestartDecidedBeforeTheImportIsClaimedRefusesTheImport, have the exporter publish an InitializeTargetMsg whose initiator is DS_ID, and assert that the NOTE_FULL_UPDATE_ENGAGED_FROM_REMOTE_START count stays at totalUpdatesStartedBefore. It is red under ieCtx = importExportContext.get() at :2602.
nitpick (non-blocking): The javadoc of aRestartDecidedBeforeTheImportIsClaimedRefusesTheImport still describes the outcome from before #1039.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ReplayDuringImportTest.java:530-532, :651-653
It says the import "ends on nothing with no exception recorded … and the total update is reported as finished". The base contains #1044, and both the case's own comment at :651-653 and the commit message say "as a failed import since issue #1039".
* thread about to run the import. Run over that broker, the import ends on the nothing
* which arrived - as a failed import since issue #1039, and as a finished one before it -
* over a suffix which has been replaced by it all the same. Here the listener is heldnitpick (non-blocking): ERR_INIT_REJECTED_SESSION_STOPPING_330 says "refused by this directory server", but the exporter reads it too.
opendj-server-legacy/src/messages/org/opends/messages/replication.properties:712-715
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:2572-2575, :2121-2127
The same message is logged locally and sent as the ErrorMsg detail. The exporter shows only getDetails(), in the task result via processErrorMsg() or through ERR_ERROR_MSG_RECEIVED_79, with no sender id. On the exporter, "this directory server … its session" therefore names the exporter and the exporter's session. This wording was my own round-1 suggestion, and this corrects it: name the refusing server, as 329 does.
ERR_INIT_REJECTED_SESSION_STOPPING_330=The total update of domain "%s" was refused by directory \
server %d: its session to the replication server is being stopped, and the entries would have \
been streamed over that session. Ask for the total update again once the session is back…f a session restart, and refuse the total update which lands across it The owner read of a session restart and the listener thread's claim of the import context for an InitializeTargetMsg share no lock, and cannot: disableService() waits for the listener thread under serviceStateLock, so the listener cannot take that lock before its claim. A restart which read no owner a few statements before the claim landed stopped the broker the import was about to read: the import ran over it and ended on the nothing which arrived, the suffix replaced by it - reported as a finished total update until issue OpenIdentityPlatform#1039, recorded as a failed import since - or, the stop landing before the full update status was published, the listener thread died on a NullPointerException in signalStatusChange(). The two now contend on the one reference. disableServiceUnlessImportInProgress() claims importExportContext with SESSION_BEING_STOPPED before it stops anything, and releases it once disableService() returns - the listener thread is gone by then. The listener's claim fails against it and the InitializeTargetMsg is refused: ERR_INIT_REJECTED_SESSION_STOPPING, which names the refusing server, to the exporter and to this server's log. An export in the context is not an owner and is stopped from under, as before; once its claim is made the listener re-reads whether the broker is stopping and refuses the total update if it is, which narrows - it is a read, so it does not close - the roads which stop the session while claiming nothing: a disable(), the shutdown of the domain, and the stop which follows an export. The answer to a total update this replica asked for itself gets the same read, and fails the task which asked for it. The claim is invisible to ieRunning(), importInProgress(), getImportExportContext() and every reader of the context; releaseIEContext() now releases only the context it is given, so an operation which fails to claim one no longer releases the context it failed against. Two test seams: the listener before its claim - or, for a total update this replica asked for, before its read of the context the request claimed - and disableService() before the broker is stopped. ReplayDuringImportTest holds the listener there, drives a restart through the gap with a change whose attempts in place are spent, and releases the listener before the stop: the import must be refused, the exporter told, the data of this replica left alone, and a stale answer to a request of this replica ignored rather than imported into the claim. A second case disables the domain across the gap, a third does the same to a total update this replica asked for, and a fourth has the restart find an export in the context and stop the session that export streams over. Each road out of the claim is pinned: the restart case fails with the read in place of the claim, the two disable cases with the read of the broker removed from their arm, the export case with any owner leaving the session alone, and the assertions around them with the claim visible to ieRunning() or to the arm of a request of this replica, with a refused request releasing it, with it never released, and with the refused import keeping its context.
7f9aded to
7418d7b
Compare
|
Thank you for round 3. All six points are taken; the head is suggestion - the self-initiated arm of question - #1049. Once both are in, #1049's contract stands: a replay restart waits out an export. suggestion - the CAS in suggestion - the readers moved to nitpick - the javadoc of nitpick - 330 names "this directory server". Taken: Checks on this head:
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: This round closes the round-2 findings in the code, not just in the text.
- The answer to a total update this replica asked for now gets the same post-claim
broker.shuttingDown()read as a remote-initiated one, and fails the task with 330 (ReplicationDomain.java:2625-2638). ERR_INIT_REJECTED_SESSION_STOPPING_330now names the refusing server,getServerId(), at all three callers (ReplicationDomain.java:2077,:2635,:2677). The exporter can see who refused.ReplayDuringImportTestpasses 10/10 at7418d7bb, including the newaDomainDisabledBeforeTheImportItAskedForStartsRefusesTheImport(local failsafe run, 128 s suite).
The owner read of a session restart and the listener thread's claim of the import context share no lock, and cannot share the one the restart holds:
disableService()waits for the listener thread underserviceStateLock, so a listener which took that lock before its claim would wait for a restart which is waiting for it. A restart which read no owner a few statements before the claim landed stopped the broker the import was about to read - and then waited for the listener thread to run that import to its end over the stopped broker:shutdown = truethere is one socket write, and the listener haspreBackendImport()to get through before it reads anything - the import ends on nothing,importLDIFreplaces the suffix with the nothing which arrived, andloadDataState()finds no base entry and computes the generationId of an empty suffix. It was reported as a finished total update with no error until Replication: a total update whose session stops before the DoneMsg ends as a finished import, with the exporter's generationId over partial data #1039 landed ([#1039] Report a total update whose session stops before the DoneMsg as a failed import #1044); on this master it is recorded as a failed import (ERR_INIT_SESSION_STOPPED_DURING_IMPORT) and the replica no longer comes up as a peer of the exporter over the nothing it holds - the data of this replica is gone either way;setNewStatus(TO_FULL_UPDATE_STATUS_EVENT), the listener thread dies on aNullPointerExceptioninsignalStatusChange()-connectedRS.get().sessionis null once the broker is stopped - afterinitialize()'sfinallyhas reported the total update as finished. This is what the first run of the new test showed; the issue describes the first shape.What this changes
ReplicationDomain.disableServiceUnlessImportInProgress()claimsimportExportContextwith a context of its own,SESSION_BEING_STOPPED, before it stops anything, and releases it oncedisableService()has returned - the listener thread is gone by then, and it is the one thread which claims a total update this replica did not ask for.restartSession()calls it in place ofsessionHasAnOwner()+disableService(): a claim which fails against an import leaves the session alone, as the read did; one which fails against an export stops the session from under it, as before - an export is not an owner, and the exporter reports the cut.SESSION_BEING_STOPPED, and theInitializeTargetMsgis refused:ERR_INIT_REJECTED_SESSION_STOPPING(330) is sent to the exporter and logged on this server, which had nothing in its log for a total update it refused. The text names the refusing server by its id: the exporter shows only the details of theErrorMsg, with no sender, so "this directory server" would have named the exporter there. A refusal published over a broker which is already stopped is silently not sent, and the exporter then learns of it from the replication server, as it did.disable()or by the shutdown of the domain, neither of which claims, and the total update which lands between the end of an export and the stop of a restart which found that export. Without it the claim wins, and what runs next publishes the full update status over a session which is gone. It is a read, not a claim - none of those roads writes anything it could fail against - so a stop which lands after it still has the import run over a session which is going down, and end as a failed import over the suffix it has replaced (Replication: a total update whose session stops before the DoneMsg ends as a finished import, with the exporter's generationId over partial data #1039). Every one of these roads had that window before this change, and has it still.initiator == getServerId()) gets the same read oncemarkInitStartReceived()has taken the context the request claimed: a restart never stops the session under it - an import owns it - butdisable()and the shutdown of the domain do, and nothing released that context on those roads. The task which asked for the total update is failed with 330. Pre-existing, and the same window as the arm above.ieRunning(),importInProgress(),getImportExportContext()and every reader of the reference: theErrorMsgandInitializeRcvAckMsgarms ofreceive(), the self-initiated branch ofinitialize()-markInitStartReceived()on the claim would return true and run the import on a static context -abortStalledInitializeFromRemote(), the export, and the monitor.releaseIEContext()takes the context to release and releases only that one.initializeFromRemote()released the context on every failure, including the failure to acquire one: adsreplication initializerun while an import or export was in progress released that operation's context -receiveEntryBytes()then reads a null context - and, with this change, would have released the claim of a restart. Pre-existing, and on the road this change makes the claim depend on.Tests
Two seams, after
setReplayDrainTimeout():setImportClaimHook()runs on the listener thread between theInitializeTargetMsgit took off the session and its claim - or, for a total update this replica asked for, its read of the context the request claimed -setServiceStopHook()insidedisableService()before the broker is stopped. Nothing else can hold either thread there.Four cases in
ReplayDuringImportTest, on the fixture of #968 (the exporter is a broker of the test, the replay queue is synchronous):aRestartDecidedBeforeTheImportIsClaimedRefusesTheImport- the listener held before its claim; a Modify whose entryUUID search never runs (ShortCircuitPluginonSEARCH) spends its attempts in place on a thread of its own, decides the restart and is held before the stop; the listener is released in between, so the broker it finds is up and only the claim can refuse it. The refusal reaches the exporter as anErrorMsgwith that text,NOTE_FULL_UPDATE_ENGAGED_FROM_REMOTE_STARTis not logged, the listener thread does not die, the session comes back after the backoff, and the entry added before is still there. While the stop is held, the exporter also sends an answer to a request of this replica which no context stands for (the request abandoned as stalled, dsreplication enable hangs initializing registration information when adding a third replica; CI Test replication step has no timeout #861), followed by a total update of its own as the point the listener is known to be past it: the answer is ignored rather than imported into the claim (that arm reading the raw reference in place ofgetImportExportContext()starts the total update, and is red). Without the claim -sessionHasAnOwner()read anddisableService()in its place - nothing refuses the total update asked for while the stop is held, and the case is red there.aDomainDisabledBeforeTheImportIsClaimedRefusesTheImport- the same hold,disable()across it,enable()after. With the re-read of the broker after the claim removed it fails on the start of the total update.aDomainDisabledBeforeTheImportItAskedForStartsRefusesTheImport- the same, for a total update this replica asked for:initializeFromRemote(), the answer held before the listener reads the context,disable()across it. With the read of the broker removed from that arm the import starts over the stopped session, and the case is red.aRestartDecidedWhileAnExportRunsStopsTheSessionItStreamsOver- the export arm. The domain exports into the exporter broker, which never reports the start of the total update, so the export stands inwaitForRemoteStartOfInit()holding the context; the same failed replay decides the restart, and the restart is held in the stop seam. The stop is reached - an export is not an owner - the export ends on the cut it reports, and the session comes back. Underif (owner != null)in place ofif (owner.importInProgress())the restart returns false, the stop seam is never reached and the case is red.What each of the four roads out of the claim is pinned by, one assertion each: the sentinel is invisible to
ieRunning()while the stop is held (getImportExportContext()returning the raw reference is red); a total update asked for while it is held is refused and does not release it (initializeFromRemote()back to the unconditionalimportExportContext.set(null)is red); the sentinel is released with the stop, so the next total update into this replica runs to its end (deleting the CAS in thefinallyis red); and the import the re-read refuses gives its context back (deleting thatreleaseIEContext()is red). The guard and the CAS of the refused-request road are red only together: with the guard in place the refused road releases nothing, so the CAS inreleaseIEContext()on its own - which keeps a second completion of the same context (theErrorMsgarm and the stalled watchdog each hold their own read of it) from clearing what was claimed in between - is not pinned; no case drives two completions.ReplayDuringImportTestis 10/10 on this head, and the twelve other classes which drive a total update or read the import/export context -SessionRestartBackoffTest,SessionRestartRequestsTest,ServerStateFlushTest,NamingConflictTest,PendingChangesTest,ReplicationDomainTest,InitOnLineTest,GenerationIdTest,ReSyncTest,UpdateOperationTest,AssuredReplicationPluginTestandMonitorTest- are 131/131,Skipped: 0.On master
The claim replaces the read #968 introduced (
sessionHasAnOwner()) and the refusal is the road that read left open a few statements wide. The branch is the one commit[#1041]onorigin/masterat26defc28ef, rebased across #977 ([#952]), #981, #982, #1044 ([#1039]), #1042 ([#1040]), and now #1010 ([#915]), #1019 ([#1014]) and #1022 ([#1021]).The one conflict of this rebase was again the tail of
replication.properties: #1019 addedWARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEER_328where this branch adds 330, and both blocks are kept, master's first. The Java sources merged on their own.With #1049
#1049 (#1048) has
runRequestedSessionRestarts()- the only replay-road caller ofrestartSession()- return at once whileieRunning(), so a restart waits out an export instead of cutting it. With both merged that is the contract: the export arm ofdisableServiceUnlessImportInProgress()is then reached only across the window between that read and the claim, andaRestartDecidedWhileAnExportRunsStopsTheSessionItStreamsOver- which holds the restart in the stop seam under an export - goes red, although the two changes touch different methods and rebase cleanly. The PR merged second drops that case and narrows the javadoc ofdisableServiceUnlessImportInProgress()to that window.Ordinal
ERR_INIT_REJECTED_SESSION_STOPPING_330: master holds up to 329 (with #1044 and #1019 merged), and #1049 holds 331 among the open PRs. 330 is free on master and on every open PR.Not in this change
restartService(), the restart a configuration change asks for, is merged as [#1040] Leave the session to a total update when a configuration change asks for a restart #1042 and still decides onsessionHasAnOwner(); the claim is the primitive it would take.signalStatusChange()on a stopped broker throws aNullPointerExceptionout ofsetNewStatus()and ends the listener thread on it. This change keeps the total update from reaching it over a stopped broker; the null session itself is pre-existing and deserves an issue of its own.Fixes #1041