[#813] Refuse to create a replica DB once the changelog shutdown has started - #820
Conversation
…ngelog shutdown has started FileChangelogDB.getOrCreateReplicaDB() read the shutdown flag only in its loop condition, before getExistingOrNewDomainMap() inserted the domain map and before the replica DB was created under the monitor of that map. A caller which read false just before shutdownDB() flipped the flag therefore inserted its domain map into a map which had already been drained, and created a FileReplicaDB nothing would ever shut down: its monitor provider stayed registered for the lifetime of the process, and its log stayed referenced. The flag is now read again inside the synchronized (domainMap) block which already guards the creation. Reading false there means shutdownDB() has not flipped the flag yet, hence has not created its iterator over domainToReplicaDBs yet either: it will see the domain map, inserted before that monitor was taken, and will have to block on the same monitor to drain it. Reading true returns null, and the loop then throws ERR_CANNOT_CREATE_REPLICA_DB_BECAUSE_CHANGELOG_DB_SHUTDOWN, which is what a caller racing a shutdown is meant to get. getExistingOrNewDomainMap() and the creation of a replica DB, now newReplicaDB(), are package private and overridable so that the test can drive the interleaving step by step: the creator is held right after it has read the flag, the shutdown is held inside the shutdown of the replica DB it drains - once the domain map has been removed and while the replication environment is still open - and the creator is then released into that window. Without the fix the test reports both symptoms: the creation succeeds, and the monitor provider of the replica DB it created stays registered.
maximthomas
left a comment
There was a problem hiding this comment.
Analysis holds up end to end. I reproduced both directions locally: as submitted the test passes 7/7; with only the 11-line guard in getExistingOrNewReplicaDB() reverted (test untouched) it fails deterministically with exactly the two symptoms you report. Also checked: one insertion site and two removal sites for domainToReplicaDBs, both removals under synchronized (domainMap) — the case analysis really is closed; the merge with #805 is clean; the new test is picked up by failsafe's default includes. Your correction of the issue's msgID 274 attribution is right (274 is an over-release, this race is an under-release).
One change requested, plus nits.
Winning branch is untested (medium)
The test covers only the losing branch (shutdown reads true → ChangelogException). The branch that carries the correctness argument — shutdown reads false under the monitor, so the drain must block and shut the new DB down — has no test.
That branch holds only because shutdownDB() builds its iterator after the CAS and ConcurrentHashMap guarantees iterators "traverse elements as they existed upon construction of the iterator". Any future refactor of the drain in opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java:354-366 — snapshotting keySet(), moving replicationEnv.shutdown() earlier, parallelising it — silently reintroduces the leak while replicaDBLosingTheRaceAgainstShutdownIsNotCreated stays green.
The seams you added make the second test cheap: hold the creator inside newReplicaDB() instead of getExistingOrNewDomainMap(), start the shutdown, assert it is blocked, release, then assert the monitor provider is gone.
Monitor assertions read JVM-global state by prefix (medium)
replicaDBMonitorNames() in opendj-server-legacy/src/test/java/org/opends/server/replication/server/changelog/file/FileChangelogDBTest.java matches any replication server and any domain in the JVM:
final String prefix = "changelog for ds(" + serverId + ")";TestNG runs many classes per JVM. Server ids 813/814 make a collision unlikely today, but the assertion is stronger and self-documenting scoped to the full registered name (FileReplicaDB.java:277-281):
toLowerCase("Changelog for DS(" + serverId + "),cn=" + domain.getMonitorInstanceName())Nits
- Cite the CHM guarantee: the new comment in
getExistingOrNewReplicaDB()says the drain "will see this domainMap" without saying why. One clause — ConcurrentHashMap iterators traverse elements as they existed upon construction — makes the load-bearing step self-contained for the next reader. deregisterMonitorProvider()bypassed: thefinallyblock'sDirectoryServer.getMonitorProviders().remove(monitorName)leaves theJMXMBeanbehind (opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java:2734-2760). Pull the provider out and pass it toDirectoryServer.deregisterMonitorProvider(provider).- Cleanup misses
DRAINED_SERVER_ID: ifsoftly.assertAll()fails, the final assertion never runs and theds(814)monitor leaks into the rest of the JVM run — thefinallyclears onlyRACING_SERVER_ID. join()swallows its timeout: a hung creator/shutdowner leaks a live changelog with open logs into later test classes with no signal.newReplicaDB()duplicates the constructor signature: will rot silently ifFileReplicaDB's constructor gains a parameter.- Helper duplication:
createCleanDir(),createCryptoSuite(),configureReplicationServer()and thecipherTransformation/keyLength/TEST_ROOT_DNfields are copied fromFileReplicaDBTestin the same package. - #818 becomes more likely: this adds a third path that leaves an empty domain map behind, and on the shutdown path that map outlives
shutdownDB()entirely while still firingcursor.addDomain(baseDN, null). Your own test exercises exactly that residue, so worth landing #818 soon after.
…creation race fix The losing branch of the race was tested, but the branch the fix relies on - a creation which reads the shutdown flag as false under the domain map monitor must have its replica DB shut down by the drain - was not. The new FileChangelogDBTest.replicaDBWinningTheRaceAgainstShutdownIsShutDownByTheDrain holds the creator inside newReplicaDB(), once the monitor provider is registered but before the DB is published into the domain map, waits for the shutdown to block on the domain map monitor, and releases the creator into the drain, which must shut the new replica DB down. The monitor assertions matched any name starting with "changelog for ds(<id>)", i.e. any domain of any replication server in the JVM: they now match the full registered name, scoped by the monitor name of the domain. The cleanup deregisters leaked providers through DirectoryServer.deregisterMonitorProvider(), which also releases the JMX MBean registered alongside, and covers both server ids. join() no longer swallows its timeout: a hung thread is interrupted and reported with its stack trace. The comment justifying the fix cites the ConcurrentHashMap iterator guarantee it relies on. The helpers copied from FileReplicaDBTest moved to FileChangelogTestFixtures, shared by both test classes.
|
Addressed in 8773e2e. Winning branch is untested — added Monitor assertions read JVM-global state by prefix — Nits:
|
|
@vharseko please fix merge conficts |
fixed |
maximthomas
left a comment
There was a problem hiding this comment.
LGTM. Both requested changes are in, and the merge is clean.
Before merging
- Wait for a Linux leg to go green — they are the only ones that set
-P precommit, so macOS and Windows never executeFileChangelogDBTest. At review time onlybuild-maven (macos-latest, 11)had reported. - Land #818 soon after: this adds a third path leaving an empty domain map behind, and on the shutdown path that map now outlives
shutdownDB()entirely while still firingcursor.addDomain(baseDN, null).
Fixes #813.
Problem
FileChangelogDB.getOrCreateReplicaDB()reads the shutdown flag in its loop condition(
:193), and everything the creation needs happens afterwards: the domain map is inserted bygetExistingOrNewDomainMap()(:230) and the replica DB is created and registered bygetExistingOrNewReplicaDB()(:275-277). MeanwhileshutdownDB()flips the flag with a CAS(
:332) and drainsdomainToReplicaDBs(:354-366).A caller which read
falsejust before that CAS therefore inserts its domain map into a mapwhich has already been drained, and creates a
FileReplicaDBwhich nothing will ever shutdown.
FileReplicaDBregisters its monitor provider in its constructor and deregisters it inshutdown()(FileReplicaDB.java:118-119and:221), and thatshutdown()is only reachedfrom the drain in
shutdownDB()and fromremoveDomain(). So the monitor provider of areplication server which has stopped stays registered for the lifetime of the process, and the
log of that replica stays referenced: its entry in the static
Log.logsCacheis pinned,together with the file handles of its log files, and the next replication server which opens
the same path in the same JVM — the test suite does this all the time — gets that stale
Loginstance back out of the cache, whose files a
removeDB()may have deleted in between.The window is narrow but real: the drain has to be in progress, since a creation which reaches
Log.openLog()afterreplicationEnv.shutdown()(:367-370) is refused byReplicationEnvironment.checkShutDownBeforeOpening()anyway.Changes
synchronized (domainMap)block which alreadyguards the creation, next to the check which covers a concurrently removed domain map.
Reading
falseunder that monitor means the CAS inshutdownDB()has not run yet, hence itsiterator over
domainToReplicaDBsdoes not exist yet either: it will see the domain map,which was inserted before the monitor was taken, and will have to block on that same monitor
to drain it. Reading
truereturnsnull, and the loop ingetOrCreateReplicaDB()thenthrows
ERR_CANNOT_CREATE_REPLICA_DB_BECAUSE_CHANGELOG_DB_SHUTDOWN, which is what a callerracing a shutdown is meant to get.
getExistingOrNewDomainMap()is package private, and the creation of aFileReplicaDBmovesinto a package private
newReplicaDB(). Both are overridable so that the tests can hold athread at each end of the race; neither is overridden in the product.
There is one insertion site and two removal sites for
domainToReplicaDBs, so the caseanalysis is closed.
Tests
FileChangelogDBTestdrives both branches of the race step by step, without a single sleep.replicaDBLosingTheRaceAgainstShutdownIsNotCreated— the branch a caller loses:provider is asserted to be registered, so that the test cannot pass by looking for a
registration it would not see anyway;
false, and is heldthere, before it inserts the domain map it needs;
domainToReplicaDBs, and is held inside theshutdown of DS(814), i.e. once that domain map has been removed and while the replication
environment is still open — held after
replicationEnv.shutdown()instead, the unfixed codewould fail for the wrong reason and the test would pass without testing anything;
replicaDBWinningTheRaceAgainstShutdownIsShutDownByTheDrain— the branch the fix relies on,which the first test cannot see: a drain refactored to no longer traverse
domainToReplicaDBsas it existed when the flag was flipped — snapshotting the keys beforehand, shutting the
replication environment down first — would silently reintroduce the leak while the first test
stayed green.
registered but before the DB is published into the domain map, still under the domain map
monitor — the exact state the unfixed code leaked from;
creator holds;
must shut that replica DB down, deregistering the monitor provider asserted registered in
step 1.
The first test, checked against the unfixed code, reports both symptoms:
The whole replication package passed as well at the initial revision of this PR — the review
round changed only the tests and a comment:
Out of scope
Three adjacent defects found while analysing this one, filed separately rather than folded in
here: #816 (
removeDomain()throws aNullPointerExceptionwhen it racesshutdownDB()),#818 (an empty domain map is left behind by a creation which bails out — the residue this fix
adds one more path to) and #819 (
FileReplicaDBleaks its log reference when its constructorcannot read the CSN limits).
The issue attributed msgID 274 (
ERR_CHANGELOG_UNREFERENCED_LOG_WHILE_RELEASING) to this raceas well. That message is logged when
Log.releaseLog()finds no entry inlogsCache, i.e. onan unbalanced release, whereas this race produces the opposite imbalance — an open which is
never released. What it does explain is the monitor provider which outlives its replication
server, and the pinned cache entry described above.