[#1040] Leave the session to a total update when a configuration change asks for a restart - #1042
Conversation
…a configuration change asks for a restart A configuration change which restarts the session of a domain for what it carries - the broker properties, the assured or fractional configuration, the attributes published to the external changelog - restarted it while a total update into this replica was reading it: restartService() and allowReconnection refused the restart under ownsItsSession() alone, and a total update into this replica never sets disabled. Made through the server configuration the restart does not end: the change holds the lock of the configuration, disableService() joins the listener thread the import runs on, and the import needs that lock to enable the backend back once its stream ends - the change never returns, and every configuration change of the server, the shutdown of the domain and every later total update wait behind it. The external changelog entry accepted every change, and the domain entry one which started an import between "acceptable" and "apply". restartService() and allowReconnection take sessionHasAnOwner(), the predicate OpenIdentityPlatform#968 gave the restart a failed replay asks for; the restart is reported as it is for a disabled domain, and the import starts the next session itself when it ends, on the configuration stored meanwhile. The listener of the external changelog entry refuses a change while a total update runs, as the listener of the domain entry does. Message 327 names the third case. ConfigChangeDuringImportTest: the change of the external changelog entry made through the server configuration while the import streams is refused - on the base it does not return, and the case captures the two stacks and interrupts it - the domain entry refuses as it did, and the two restarts reached directly leave the session to the import and report it.
60ad913 to
c8dcf56
Compare
|
@maximthomas rebased onto master: #968, which this branch was stacked on, merged as On the rebased head, one JVM per class: |
A configuration change which restarts the session of a replication domain for what it carries restarts it while a total update into this replica is reading it.
LDAPReplicationDomain.restartService()refuses the restart underownsItsSession()-shutdown.get() || disabled- andreadAssuredConfig()/readFractionalConfig()takeallowReconnectionfrom the same predicate; a total update into this replica never setsdisabled:preBackendImport()takes the backend away without disabling the domain, because the import streams over the session, on the listener thread, and the domain can not stop the session it is importing over.The issue describes the consequence as the replay road of #968 has it - the broker stops,
receiveEntryBytes()returnsnullonshuttingDown()without recording an exception, and the import ends on the entries which had arrived, reported as finished. On the configuration road it is worse than that, and the case below shows it: the change holds the lock of the configuration (ConfigurationBackend.replaceEntry,synchronized (configLock)) for the length ofapplyConfigurationChange(),disableService()stops the broker and then joins the listener thread, and the listener thread - the import - ends its stream and goes to enable the backend back,closeBackendImport()→TaskUtils.enableBackend()→ an internal modify of the backend entry → the sameconfigLock. The change never returns;configLockandserviceStateLockstay held, so every configuration change of the server after it waits,disable(), the replay threads'restartSession()andshutdown()of the domain wait -stop-dswith them - the backend stays deregistered, and the import context stays acquired, so every later total update is refused as a simultaneous one.The roads in. The listener of the domain entry refuses a change while
ieRunning(), which leaves an import which starts between "acceptable" and "apply" - inside the lock, so the listener thread is then parked onpreBackendImport()'sdisableBackend(), and the join is the same. The external changelog entry was not covered at all:ExternalChangelogDomain.isConfigurationChangeAcceptable()returnedtrueunconditionally, anddsconfig set-external-changelog-domain-prop --set ecl-include:...on a domain being initialized wentapplyConfigurationChange()→domain.changeConfig(eclIncludes, ...)→restartService()with nothing asking whether an import was reading the session.What this changes
restartService()andallowReconnectiontakesessionHasAnOwner()-ownsItsSession() || importInProgress(), the predicate [#956] Tell a failed entryUUID search apart from an entry which is not there #968 gave the restart a failed replay asks for. The owner spans the import context, fromacquireIEContext()toreleaseIEContext(), and bothTaskUtils.*Backend()calls of the import lie inside it, so no join underconfigLockis left. The restart is reported the way it is for a disabled domain -onSessionRestartSuppressed(),adminActionRequiredand 327 on the domain listener - and the import starts the next session itself when it ends, from the state it loaded; the broker's stored configuration, the assured configuration and the attributes published to the external changelog are all read as that session comes up (connectAsDataServer(),StartSessionMsg).ExternalChangelogDomain.isConfigurationChangeAcceptable()refuses the change whiledomain.ieRunning(), withNOTE_ERR_CANNOT_CHANGE_CONFIG_DURING_TOTAL_UPDATE, as the listener of the domain entry does.ieRunning()rather thanimportInProgress()on purpose: it is the predicate the domain listener uses, an export is covered by both the same way, andimportInProgress()is protected inReplicationDomain- not visible from this class.Base
Rebased onto master after #968 merged (
9300ffec88, the round-5 head00798abf33this branch sat on, squashed - the trees are identical). One commit, the patch unchanged (git range-diffreports it=);sessionHasAnOwner()andimportInProgress()as an owner are #968's. #977 (eef0757515), the one commit of master on top of #968, touchesLDAPReplicationDomain.javain the checkpointer and does not meet this change.Tests
ConfigChangeDuringImportTest, a class of its own with the fixture ofReplayDuringImportTest: theuserRootbackend, a real replication server, a broker of the test as the exporter which says when the entries arrive, the change made while the import waits for them.aChangeOfTheExternalChangelogEntryIsRefusedWhileATotalUpdateRuns- the domain configured through the server configuration, so that the listeners of its entries are registered and the change goes throughconfigLock;ds-cfg-ecl-includereplaced on thecn=external changelogentry from a thread of the test, the waydsconfigdoes it. The change is refused withUNWILLING_TO_PERFORMand the total-update reason, the import runs to its end, and the attributes are not applied. On the base the change does not return: the case waits 30 s, captures the two stacks above, interrupts the change - which gives up the join, so that the domain can be taken down - and fails on them.aChangeOfTheDomainEntryIsRefusedWhileATotalUpdateRuns- the twin on the domain entry (ds-cfg-assured-type), as it was before this change; pinned so that the two entries keep answering the same thing.aChangeOfTheDomainConfigurationLeavesTheSessionToTheImport-applyConfigurationChange()reached directly with an assured configuration which needs a restart, the way a change accepted before the total update reaches it:SUCCESS,adminActionRequired, 327, the listener thread the import runs on is the same thread afterwards, every exported entry arrives, and the assured mode is stored. On the base:adminActionRequiredis false, the restart went through.aChangeOfTheExternalChangelogAttributesLeavesTheSessionToTheImport-changeConfig(eclIncludes, ...)reached directly: the listener thread is the same afterwards, every entry arrives, the attributes are stored. On the base: a new listener thread.Base = the head of #968 without this commit: 1/4 (the domain-entry twin), the three others red each on its own assertion. Mutations, each run against the final class:
restartService()back onownsItsSession()- 3/4,aChangeOfTheExternalChangelogAttributesLeavesTheSessionToTheImportred on the listener thread;allowReconnectionback onownsItsSession()- 3/4,aChangeOfTheDomainConfigurationLeavesTheSessionToTheImportred onadminActionRequired. The listener's refusal is what the base run measures for the first case: with it gone the change does not return.Neighbours on the fixed tree, one JVM per class:
LDAPReplicationDomainConfigChangeTest9/9,SessionRestartTest2/2,ReplayDuringImportTest3/3,InitOnLineTest10/10,GenerationIdTest4/4, andConfigChangeDuringImportTest4/4, nothing skipped.On the head rebased onto master (
c8dcf56e1f), the same classes andServerStateFlushTestof #977:ConfigChangeDuringImportTest4/4,LDAPReplicationDomainConfigChangeTest9/9,SessionRestartTest2/2,ReplayDuringImportTest3/3,InitOnLineTest10/10,GenerationIdTest4/4,ServerStateFlushTest4/4, nothing skipped.Not in this change
acquireIEContext()share no lock, and this road has the same window: a change which readsimportInProgress()false underserviceStateLocka few statements before the listener claims a remote-initiatedInitializeTargetMsgjoins a listener thread which is about to needconfigLock. The CAS underserviceStateLockwhich Replication: the owner read of a session restart and the listener's claim of a remote-initiated import share no lock #1041 asks for closes both.ExternalChangelogDomain.applyConfigurationChange()returns plain success and reads nosessionRestartSuppressed, and the next change of the domain entry resets the flag as none of its business. Pre-existing to [#926] Restart the session of a replication domain in one place, under the lock and the generation #974 for a disabled domain; with the listener refusing the change, what is left of it is the window between "acceptable" and "apply".Fixes #1040