Repository navigation
An empty domain map is left behind by a replica DB creation which bails out #818
Description
Activity
- addedconcurrencyThread-safety / race-condition bugsThread-safety / race-condition bugs
on Aug 3, 2026 Three corrections from a deeper pass over the paths involved, before the fix lands:
1. The
:266bail-out leaves no empty map behind today. The identity checkdomainToReplicaDBs.get(baseDN) != domainMapcan only fail because a remover has already unmappeddomainMap— so nothing stays behind on that path, and the retry loop ofgetOrCreateReplicaDB()inserts a fresh map and populates it. The lasting empty map starts to exist only together with the shutdown check of #813, which bails whileget(baseDN) == domainMapstill holds.2. The path which does leave an empty map behind today is a
FileReplicaDBconstructor failure.new FileReplicaDB(...)at:274throwsChangelogExceptionon an I/O error, and the exception propagates out ofgetOrCreateReplicaDB()past the freshly inserted, already announced map. The cleanup must cover that exit too, not only the tworeturn nullbail-outs.3. The unconditional
remove(baseDN, domainMap)can reintroduce the leak of #813.domainMapis not necessarily the map the caller inserted: the happy path ofgetExistingOrNewDomainMap()returns a pre-existing map, which may hold the replica DBs of other serverIds. When the shutdown bail-out fires on such a map, removing it races the weakly consistent iterator drainingdomainToReplicaDBsinshutdownDB(): the safety argument above only covers an iterator which has already returned that map and is blocked on its monitor, not one which has not reached its bin yet — the latter never sees the map, and its replica DBs are never shut down, which is exactly the leak of #813. The remove must therefore be guarded:if (domainMap.isEmpty()) { domainToReplicaDBs.remove(baseDN, domainMap); }
An empty map is the only one worth dropping, and the only one the drain can safely miss. Wrapping the creation in
try/finallywith that guard covers all three exits at once — the identity bail-out (where the conditional remove is a no-op), the shutdown bail-out of #813, and the constructor failure — and keeps a map which just received its replica DB, since it is no longer empty by the time thefinallyruns.Two smaller notes.
Iterator.remove()of aConcurrentHashMapremoves the key unconditionally rather than the entry it returned, so on a concurrently remapped key the drain removes the new map — harmless once the shutdown check of #813 is in, since a post-flip creation bails before putting anything into it, but the "no-op" claim above is exact only for a key which is no longer mapped at all. And dropping the map does not retract theaddDomain()already broadcast at:237-240: registered cursors keep the announced domain, which is harmless by the same lazy-creation logic which lets a cursor hold a domain with no replica DBs yet.- added 5 commits that reference this issue
on Aug 4, 2026
FileChangelogDB.getOrCreateReplicaDB()inserts a domain map, and announces the domain to everyregistered multi domain cursor, before it can know whether it will create anything in it:
The caller can then bail out of the creation without ever putting a replica DB in that map:
today when the domain map was concurrently removed (
:266), and, once #813 is fixed, when ashutdown was initiated. The empty map stays in
domainToReplicaDBsfor the lifetime of thechangelog. On the shutdown path it outlives
shutdownDB()altogether, which draineddomainToReplicaDBsbefore that map was inserted into it.What that empty domain map is then worth:
domainToReplicaDBs, sogetCursorFrom()adds it to every multi domaincursor created afterwards (
:697-704), i.e. the external changelog walks a domain which hasno replica DB at all;
getDomainMap(),getDomainOldestCSNs()andgetDomainNewestCSNs()answer for it as theywould for an unknown domain, so those are unaffected;
clearDB()iterates the keys and callsremoveDomain()on it (:427-430), which reachesreplicationEnv.clearGenerationId()for a domain the changelog holds nothing for — and, aftera shutdown, on an environment which is closed.
Proposed fix
Drop the map the caller inserted when the creation it was inserted for does not happen:
becomes, together with the shutdown check of #813, a bail-out which first does
The conditional remove is safe under the monitor of
domainMap: a shutdown which had alreadyseen that map only finds the entry gone when it acquires the monitor, and
Iterator.remove()on an entry which is no longer mapped is a no-op.
Found while analysing #813.