[#896] Pin the replay of RootContainer.open, and the give-back of an open which is not replayed - #1002
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The shape is the right one and the description argues it from the code, not from intent.
- Opening inside the write and registering after the commit follows #907 / #914 exactly, and "cannot fail: the base DNs come from a set, and the map of a root container being opened is empty until here" (
RootContainer.java:160-161) is a proof, not a hope —ERR_ENTRY_CONTAINER_ALREADY_REGISTEREDreally is unreachable from this path now. closeSilently(opened); opened.clear()at the top of every attempt (:154-155) is the give-backBackendImpl.changeBaseDNTreesalready does, applied where the replay loop actually re-enters.ReplayedOpenTestdrives the replay throughPDBStorage.write's own loop rather than a secondwrite()call, so the case reads the engine's contract, not a model of it; and the "Which engines reached it" section names the jdbc guards (isExistsTable,isExistsIndex) that keeppartlyCommittedfalse — I checked, postgres'screate index if not existsis guarded too (JDBCStorage.java:3627).
issue (blocking): A container enters opened only after getHighestEntryID returns, so a failure of that read leaves it outside every unwind.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/RootContainer.java:253-255, :154, :174, :179
openEntryContainer (:253) constructs the container — its constructor registers config.addPluggableChangeListener(this) (EntryContainer.java:470) and open() registers one listener per index — then ec.getHighestEntryID(txn) (:254) runs txn.openCursor(id2entry) + positionToLastKey (EntryContainer.java:699-702), a storage read, and only then opened.add(ec) (:255). If the read throws, ec is in neither opened nor the registry. On jdbc, a restart of an existing backend with a dropped connection (or a class-40 conflict) on that SELECT has committing=false, partlyCommitted=false, so replayReason (JDBCStorage.java:3117) replays: the replay closes base DNs 1..k-1, opens base DN k again and registers a second container — two containers for one base DN listening on the backend cfg, the stale one applying changes to index objects the live one does not own. On the give-up road (PDB: PersistitException from the traverse) the container's listeners simply leak — the "wrong error for a silent leak" trade the description says the PR avoids. Pre-existing on the give-up road; new in shape on the replay road, where master failed loudly.
EntryContainer ec = openEntryContainer(baseDN, txn, accessMode);
opened.add(ec);
EntryID id = ec.getHighestEntryID(txn);Pin: a twin of ConflictingAtTreesOf whose openCursor throws on SECOND's id2entry, then verify(backend.configuredWith, times(2)).removePluggableChangeListener(any()) after the open — red at head (only FIRST is given back), green with the add ahead of the read.
Note: #999 (fd50d19, open) closes the same window on the same lines by registering before the read (RootContainer.java:295-296 there); whichever of the two rebases over the other, keep the add/register ahead of getHighestEntryID.
suggestion (non-blocking): The catch (StorageRuntimeException) arm's closeSilently(opened) is reached by no case; deleting it leaves all three green.
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/RootContainer.java:170-175, opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedOpenTest.java:285, :379
Cases 1-2 replay and commit, entering neither arm. Case 3 throws UnreplayableFailure extends Exception (:285); PDBStorage.write:692 rethrows it unchanged, so it lands in the catch (Exception) arm (:177-180), whose closeSilently satisfies the times(2). Production's give-up road is the other arm: PDB's spent-cap StorageRuntimeException (PDBStorage.java:718-737, no cause, so unwrap rethrows it as is) and jdbc's rethrown transaction failure (JDBCStorage.java:3060-3061). Traced by exception type, not run.
// ReplayingStorage.ConflictPoint
/** Once the operation has run to completion, as the failure an engine reports when its replays are spent. */
SPENT
void failAsSpent()
{
arm(ConflictPoint.SPENT, 1);
}
// ReplayingStorage.write, after writeOperation.run(txn)
if (armed == ConflictPoint.SPENT)
{
throw new StorageRuntimeException("the replays are spent"); // no cause: PDBStorage.write rethrows it as is
}A copy of anOpenWhichIsNotReplayedGivesUpTheEntryContainersItOpened armed with failAsSpent() and the same times(2) kills the delete mutant at :174. Or: one arm, so the existing case pins the only door —
catch (Exception e)
{
closeSilently(opened);
throw e instanceof StorageRuntimeException ? (StorageRuntimeException) e : new StorageRuntimeException(e);
}suggestion (non-blocking): The first case injects a bare RollbackException from openTree, a shape the engine never delivers, and asserts nothing of the give-back on its own road.
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedOpenTest.java:471-478, :130-141
PDBStorage wraps every PersistitException | RollbackException into StorageRuntimeException at each store site (:445, :604, …), and EntryContainer.open's catch (StorageRuntimeException) (EntryContainer.java:559-564) closes the half-open container before PDBStorage.write:683-686 unwraps the cause and :688 replays. Bare, that catch does not match: SECOND of attempt 1 is neither closed nor in opened, its five constructor listeners stay on the mock cfg for the rest of the class, and the case checks attempts, base DNs and trees only — the delete closeSilently(opened) mutant at :154 is green on this case (case 2 kills it).
// ConflictingAtTreesOf.openTree
if (conflictingPrefix.equals(name.getBaseDN()))
{
// what PDBStorage delivers: the engine's RollbackException wrapped, which EntryContainer.open unwinds on
throw new StorageRuntimeException(new RollbackException());
}// openIsReplayableWhenTheTransactionConflictsWhileTheSecondBaseDNIsOpened, after openBackend()
// SECOND of the first attempt by its own close(), FIRST by the replay's closeSilently(opened)
verify(backend.configuredWith, times(2)).removePluggableChangeListener(any());issue (non-blocking): The root container's own listener leaks on a failed open().
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/RootContainer.java:108, :366, opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java:1240-1249
The constructor registers config.addPluggableChangeListener(this) (:108); only close() removes it (:366); newRootContainer drops the root container on a failed open() without close(). The new catch arms give back the entry containers and leave this one, so every failed enable keeps one RootContainer on the backend cfg for the JVM's life. Pre-existing; not among the #993 items the description names, and the times(2) in the third case fixes it as expected behaviour.
catch (StorageRuntimeException e)
{
closeSilently(opened);
config.removePluggableChangeListener(this);
throw e;
}(same in the catch (Exception) arm; the case's times(2) becomes times(3)). Or: rc.close() in newRootContainer's catch, which also takes #993's "storage left open" — entryContainers is empty on that road, so close() only removes the listener and closes the storage.
nitpick (non-blocking): JDBCStorage.replayReason's javadoc, and its copy in JDBCStorageRetryTest, state the pre-PR contract of RootContainer.open as current fact.
opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java:3099-3102, opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/JDBCStorageRetryTest.java:276-279
"opens and registers every entry container of every base DN in one write: replayed after the trees of the first base DN were created and committed, it registers that base DN a second time and fails with ERR_ENTRY_CONTAINER_ALREADY_REGISTERED" — this PR makes every clause false and touches neither file. The partlyCommitted rationale stands on the mysql/oracle DDL auto-commit alone; rewrite the example as history ("before #896, …") or drop it.
nitpick (non-blocking): ConflictPoint.COMMIT and the second case say the conflict is "reported by commit()"; the harness raises it after run() returns, before the commit.
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedOpenTest.java:302-303, :149, :381
throw new RollbackException() (:381) runs while PDBStorage.write:679 operation.run(this) is still on the stack; :680 txn.commit is never reached on the conflicting attempt. Same catch arm on PDB, so wording only — but a jdbc-shaped harness, where committing changes the answer, would be modelled wrongly from it. "Once the operation has run to completion, before the commit" is what the code does.
|
Fixed in 4916312.
Re-run green: |
…the give-back of an open which is not replayed Storage.write replays its operation after a transaction conflict, and RootContainer.open opens and registers the entry container of every base DN inside one such write. A replay used to meet every base DN the attempt it replaced had reached already registered, and failed with ERR_ENTRY_CONTAINER_ALREADY_REGISTERED - on a message which said nothing about the conflict that caused the replay - leaving the configuration listeners of the abandoned containers behind. The fix of issue OpenIdentityPlatform#993 (PR 999) takes that road away: every attempt begins by unregistering and closing what the previous one registered, a container is registered before its first read can throw, and an open which fails gives back the containers of its attempt, the root container's own listener and the storage. Nothing pinned that from the replay's side. ReplayedOpenTest drives the replay through PDBStorage.write's own retry loop, with a storage which raises PersistIt's RollbackException the way the engine delivers it: a conflict while the second base DN is being opened, a conflict once the operation has run to completion, and a failure which is not replayed at all - whose give-back now takes the root container's listener with the two entry containers. The javadoc of JDBCStorage.replayReason, and its copy in JDBCStorageRetryTest, gave that failure as the current contract of RootContainer.open; it is history now.
4916312 to
bb5d3af
Compare
|
#999 merged first ( Rebased over it, the
One commit now, Re-run green on master + this head: |
Fixes #896
RootContainer.openopens and registers the entry container of every base DN inside a singlestorage.write, andStorage.writereplays its operation after a transaction conflict - "In case ofa write operation rollback, implementations must ensure the write operation is retried until it
succeeds", whose
WriteOperationis required to be idempotent. No rollback reaches the registry, soa replay found every base DN the attempt it replaced had reached already registered and failed with
ERR_ENTRY_CONTAINER_ALREADY_REGISTERED: the backend did not open at all, and what the operator sawsaid nothing about the conflict that caused the replay. Every attempt also left the configuration
listeners of its entry containers - five per container, plus one per attribute index and per VLV
index - registered on the backend configuration.
What is in the PR now
#999 (issue #993) merged first and closes this road in
RootContaineritself: every attempt ofopenAndRegisterEntryContainersbegins by unregistering and closing what the attempt it replaces hadregistered, a container is registered before its first read (
getHighestEntryID) can throw, andgiveUpAfterFailedOpengives back the containers of an open which fails together with the rootcontainer's own listener and the storage. Rebased over it, the
RootContainerchange this PR carried(open inside the write, register after the commit) is gone; what is left is the pin, which master
lacked:
ReplayedOpenTest, built on the harnessReplayedConfigChangeTestintroduced in [#907] Change the base DNs of a pluggable backend outside the write the storage replays #914 - a storagewhich raises PersistIt's own
RollbackException, wrapped the wayPDBStoragedelivers it, frominside the operation, so the replay is driven by
PDBStorage.write's retry loop rather than by asecond call to it:
attempt being replaced - the case the report is written from;
operation that ran to completion, and asserts that the replay gave up both containers of the
previous attempt;
[#993] Register an entry container's configuration listeners only once it has opened #999, no root container listener either: three
removePluggableChangeListenercalls, not two.JDBCStorage.replayReason's javadoc, and its copy inJDBCStorageRetryTest, described the failureof a replayed
RootContainer.openas its current contract; they give it as history now.The first two cases fail on master before #999 with
ERR_ENTRY_CONTAINER_ALREADY_REGISTEREDwrapped inERR_OPEN_ENV_FAIL, the third with no listener ever deregistered; all three are green on master as itis, with no change to
RootContainer.Which engines reached it
RollbackException, bounded since PDBStorage.write() replays a rolled-back transaction without any bound, and configuration changes hold an entry container's exclusive lock across it #921 / [#921] Bound the transaction replay of PDBStorage.write() #937 but replayed all the same.openTreeis guarded (isExistsTable,isExistsIndex, the catalog row committed on a connectionof its own), so the attempt commits nothing,
partlyCommittedstays false andreplayReasonreplays both a class 40 conflict and a dropped connection. The storage layer keeps that write
replayable deliberately - see the comments at
openTreeandenrolInCatalogabout "a deadlock atthe twentieth tree" - so the Java side was the only thing breaking the contract.
Tests
Run green on master + this PR:
ReplayedOpenTest(3),ReplayedConfigChangeTest(20),FailedBackendOpenTest(8),PersistentCompressedSchemaTest(9),PDBStorageTest(14),JDBCStorageRetryTest(99). The jdbc engine suites need Docker and were not run locally; the changecarries no jdbc specific code.
Review round 2
openEntryContainersadded a container toopenedbeforegetHighestEntryIDran rather thanafter: a failure of that read left the container in neither the give-back nor the registry, the
same shape [#993] Register an entry container's configuration listeners only once it has opened #999 already used for the sibling leak of A ConfigException while opening an EntryContainer leaves its five configuration listeners registered on a half-open container #993.
open's twocatchblocks, which differed only in whether the exception they were handed alreadywas a
StorageRuntimeException, became one.ReplayedOpenTestraises the conflict the wayPDBStorageactually delivers it - aRollbackExceptionwrapped in aStorageRuntimeException, not the bare oneEntryContainer.open'sown catch cannot unwind on - and the first case asserts the give-back of both entry containers a
replayed attempt leaves behind, not only the second case's.
JDBCStorage.replayReason's javadoc, and its copy inJDBCStorageRetryTest, describeRootContainer.open's prior behaviour as history rather than as current fact.Review round 3
#999 merged with the shape the round 1 note pointed at ("whichever of the two rebases over the other,
keep the add/register ahead of
getHighestEntryID"), and with the root container's own listener inthe give-back - the non-blocking item of round 1, out of scope here then. The
RootContainerchangeof this PR is dropped for it (see above), the third case expects the third listener removal, and the
title and this description say what the PR is now. One commit.