[#926] Restart the session of a replication domain in one place, under the lock and the generation - #974
Conversation
a0eca19 to
aadee35
Compare
|
Rebased onto
disabled = false;
boolean started = false;
try
{
enableService();
started = true;
}
finally
{
if (!started)
{
disabled = true;
}
}Leaving the line where the merge put it would not have compiled either: The field block - #944, #945, #948.
disabled = true;
disableService(); // This will cut the session and wake up the listener
awaitReplayDrained();
state.save();
state.clearInMemory();Green on the rebased branch:
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: The fix is real and the consolidation is the right shape.
I ran SessionRestartTest at the merge-base (5d176c69): both cases fail there, and the road is reached synchronously from both disable() and shutdown(), so the two states the class covers are the two that matter. Moving the stop/start pair into disableService()/enableService() under serviceStateLock, with the generation bumped by the pair itself, removes the last unlocked pairs from readAssuredConfig()/readFractionalConfig(); "the configuration is applied either way" is the right contract for a domain that owns its session; and the description says why isListenerShuttingDown() can go — every restart is now counted — which made the guard change checkable rather than something to take on faith.
issue (blocking): The isSessionRestartable() arms of readAssuredConfig() and readFractionalConfig() are pinned by no test.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:3949 (readAssuredConfig)
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:851 (readFractionalConfig)
SessionRestartTest.changeEclIncludes() swaps only the ECL child on the same DomainFakeCfg, so needReconnection(config) is false and neither arm carrying the new term is evaluated; the assured/fractional suites change config on a live domain only. Deleting && isSessionRestartable() at L851, or reverting L3949-3960 to the unconditional disable/assign/enable, keeps every suite the PR names green — and that is the dsconfig ... assured-type during import-ldif road the description opens with.
A third case in the same harness pins it:
@Test
public void assuredChangeOnDisabledDomainIsAppliedWithoutASession() throws Exception
{
// RS + domain.start() + assertTrue(domain.isConnected()) as in the two existing cases
domain.disable();
final SortedSet<String> replServers = new TreeSet<>();
replServers.add("localhost:" + rsPort);
final DomainFakeCfg assuredCfg = new DomainFakeCfg(baseDN, DS_ID, replServers,
AssuredType.SAFE_DATA, 1, GROUP_ID, 1000, new TreeSet<String>()); // needReconnection() == true
assertEquals(domain.applyConfigurationChange(assuredCfg).getResultCode(), ResultCode.SUCCESS);
assertTrue(domain.isAssured(), "the configuration is applied either way");
assertFalse(domain.isConnected(), "a disabled domain must not get its session back from a config change");
}note: pre-existing and not yours — assuredConfig = config sits inside the needReconnection() branch, so a change of assured-timeout alone is never applied until a reconnection. Out of scope here.
suggestion (non-blocking): Add a positive twin — a case where the restart does happen and is observed on the RS side.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartTest.java:129
getEclIncludes() at L129 is set at ReplicationDomain.java:3444 before restartService() is called, so the mutant restartService() {} — or isSessionRestartable() { return false; } — keeps every suite green (GroupIdHandshakeTest's assertion is also satisfied by the MonitorMsg best-RS re-election). The restart carries one thing only, the new eclIncludes/groupId to the RS and on to the other DSs via TopologyMsg; that road is what would silently break. The RS-side observable is a new DataServerHandler after the reconnect, so look it up inside the retry:
changeEclIncludes(domain, domainCfg); // live domain, no disable()
new TestTimer.Builder().maxSleep(5, SECONDS).sleepTimes(100, MILLISECONDS).toTimer()
.repeatUntilSuccess(new CallableVoid()
{
@Override
public void call() throws Exception
{
final DataServerHandler ds = replicationServer.getReplicationServerDomain(baseDN)
.getConnectedDSs().get(DS_ID);
assertNotNull(ds);
assertThat(ds.toDSInfo().getEclIncludes()).contains("cn");
}
});issue (non-blocking): The lock-and-generation mechanism in the title is pinned by nothing — a follow-up issue is fine by me.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3286 (restartSession)
src/test has zero references to sessionRestartRequested, runRequestedSessionRestarts or restartSession(, and the new test never has a replay thread ask for a restart. Deleting both sessionGeneration++, or replacing getSessionGeneration() != stoppedSession by false, stays green. The guard now rests on the generation alone (the dropped isListenerShuttingDown() is reasoned in the description, no objection), so the first test that reaches it should be the one that shows a stale claim declining to restart and a current one restarting.
todo: The sessionGeneration javadoc states an exclusivity the code does not keep.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:388
It says the session is stopped or started by "only disableService() and enableService()", and the description says the only uncounted start is start(). Neither holds: the constructor's startPublishService() (LDAPReplicationDomain.java:758) and the public startListenService() (L3261) start pieces of the session without a bump. Both run before a replay thread can hold a claim, so nothing breaks — but the invariant as written is false. Suggested:
* Bumped every time {@link #disableService()} stops the session or {@link #enableService()}
* starts it, both under {@link #serviceStateLock}. The starts at domain startup -
* startPublishService() from the constructor, startListenService() from start() - are not
* counted: no replay thread exists yet to hold a claim on that session.todo: The isSessionRestartable() javadoc promises a session that does not always come back.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3949
"is read as the domain starts its session back" — enable() returns before disabled = false when loadDataState() throws (L4122-4134), and the flag stays set for the life of the server (#966). Suggested last sentence:
* The configuration such a change carries is applied all the same; the session is not
* restarted, and comes back only when the domain is enabled again.suggestion (non-blocking): Say something when the change is stored but the restart is skipped.
opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java:3464 (restartService)
Today restartService(), readAssuredConfig() and readFractionalConfig() all take the not-restartable arm silently and applyConfigurationChange() answers SUCCESS; on a domain stuck disabled (#966) the operator has no line telling them the new assured/fractional/ECL settings are waiting for a restart. One NOTICE (a logger.info(LocalizableMessage) reaches logs/errors) at the three sites, or one in isSessionRestartable()'s false arm, would do. Not a regression — at the base the same change brought up a session with an empty ServerState, which was worse.
nitpick (non-blocking): assertTrue(domain.isConnected()) straight after start() with no retry.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartTest.java:58 (also L93)
In practice deterministic — ReplicationBroker.start() connects synchronously and the RS is bound first — so take it or leave it; GenerationIdTest wraps the same assert in repeatUntilSuccess.
…n in one place, under the lock and the generation OpenIdentityPlatform#959 took the road ExternalChangelogDomain.applyConfigurationChange() reaches the session by - domain.changeConfig(Set, Set) -> restartService() - under serviceStateLock, and had a domain which owns its session refuse the restart. What it left open is the counter: the restart it runs is disableService() plus enableService() with no sessionGeneration bump, so a replay thread which stopped the session and is waiting out its backoff cannot tell that restart from nothing, and the guard in restartSession() still reads isListenerShuttingDown() to stand in for it. serviceStateLock and sessionGeneration move from LDAPReplicationDomain to ReplicationDomain, next to sessionLock, and the counter is bumped by disableService() and enableService() themselves - both final and taken under the lock now - rather than by hand at each place which stops or starts a session, so every restart in the hierarchy is counted and no caller can forget to. A start which throws leaves the generation where it was, so the thread which stopped that session still owns it and may bring it back. The guard in restartSession() is the generation alone: isListenerShuttingDown() stood in for the restarts the counter could not see, and there are none of those left. The starts at domain startup - startPublishService() from the constructor, startListenService() from start() - stay uncounted: no replay thread exists yet to hold a claim on that session. restartService() and readAssuredConfig() take their stop, change and start under the lock as readFractionalConfig() does, so a pair is atomic wherever it is called from rather than only where applyConfigurationChange() holds the lock around it. SessionRestartTest drives the configuration road the issue names against a live replication server, for a domain disabled for a total update and for one which has shut down. Fixes OpenIdentityPlatform#926.
aadee35 to
9afae36
Compare
|
Rebased onto issue (blocking) - the note - suggestion - a positive twin observed on the RS side. Right that nothing observes the restart itself: issue (non-blocking) - the lock and the generation are pinned by nothing. Still true, and after the rebase it is all the PR does, so I am not hiding behind "follow-up is fine": one issue for the three tests - a todo - todo - suggestion - say something when the restart is skipped. #959: nitpick - Two more things the rebase had to do: |
Fixes #926.
ExternalChangelogDomain.applyConfigurationChange()reached the session by a road of its own:domain.changeConfig(Set, Set)->ReplicationDomain.restartService(), which wasdisableService()plusenableService()with neitherserviceStateLocknor the session generation #892 gave the domain. #959 ([#943]) closed the first half of that: the ECL road runs underserviceStateLocknow -changeConfig(Set, Set)andrestartService()are overridden under it - and a domain whichownsItsSession(), shutting down or disabled for a total update, refuses the restart and reports it throughonSessionRestartSuppressed()asadminActionRequired. What it left open is the counter: the restart it runs is stilldisableService()+enableService()with nosessionGenerationbump, so a replay thread which stopped the session and is waiting out its backoff cannot tell that restart from nothing, and the guard inrestartSession()still readsisListenerShuttingDown()to stand in for it.What changed
The lock and the generation live where the session does.
serviceStateLockandsessionGenerationmove fromLDAPReplicationDomaintoReplicationDomain, next tosessionLock. The counter is bumped bydisableService()andenableService()themselves - bothfinaland taken under the lock now - rather than by hand at each of the places which stop or start a session, so every restart in the hierarchy is counted and no caller can forget to. A start which throws leaves the generation where it was, so the thread which stopped that session still owns it and may bring it back.The guard in
restartSession()is the generation alone.isListenerShuttingDown()was there to stand in for the restarts the counter could not see -changeConfig(),readAssuredConfig()- and there are none of those left. The starts at domain startup -startPublishService()from the constructor,startListenService()fromstart()- stay uncounted: no replay thread exists yet to hold a claim on that session. The method itself stays:processUpdate()still reads it.A pair is atomic wherever it is called from.
restartService()andreadAssuredConfig()take their stop, change and start underserviceStateLock, asreadFractionalConfig()already does, rather than only whereapplyConfigurationChange()holds the lock around them. TheownsItsSession()guard,onSessionRestartSuppressed()and theadminActionRequiredreport are #959's and are kept as they are; theisSessionRestartable()this PR used to carry was the same predicate, and is gone.Tests
SessionRestartTestdrives the road the issue names -applyConfigurationChange()->ExternalChangelogDomain->changeConfig()->restartService()- against a live replication server, for a domain disabled for a total update and for one which has shut down. It failed against the base this PR was opened on; on master since #959 that road is refused byownsItsSession(), and the test is kept as the coverage of it which goes through the ECL child of the domain configuration and throughshutdown().LDAPReplicationDomainConfigChangeTest.serviceStateLockOf()follows the field toReplicationDomain, orexternalChangelogConfigurationChangesTheSessionUnderTheServiceStateLockwould fail onNoSuchFieldException.What this does not close
src/testhas a replay thread ask for a restart, so a stale claim declining to restart and a current one restarting are exercised by nothing; arestartService()which did not move the counter would go unnoticed. Follow-up, together with the RS-side observation of a restart which does happen and a fractional twin ofassuredConfigurationIsAppliedToADomainWhichOwnsItsSession- see the review.LDAPReplicationDomain.isConfigurationChangeAcceptable()refuses a configuration change whileieRunning();ExternalChangelogDomain.isConfigurationChangeAcceptable()returnstrueunconditionally, so adsconfig set-external-changelog-domain-prop --set ecl-include:...during aninitialize-from-remote-serverrestarts the session which is carrying the initialization. I found no way to holdieRunning()open deterministically without a test-only hook in production code, so it is left out rather than shipped untested.Review round 1
Rebased onto
f559b0907a, over #959 ([#943]), #970 ([#951]), #972 ([#967]), #973 ([#928]) and #975 ([#927]). #959 landed in the same region between the base this was opened on and the review, and takes three of the review's points with it; the description above is rewritten for what remains, and the javadoc ofserviceStateLockcarries the lock-ordering paragraph #972 put on it. The point-by-point answer is in the comments.