Conversation
…, not which thread hands it out Fixes OpenIdentityPlatform#1036. getNextUpdate() hands a parked change to whichever replay thread calls it first once the changes before it have left the pending changes, and the thread which parked it calls it on its own way out - so the two OpenIdentityPlatform#958 tests which asserted isSameAs(replayingParent) failed whenever the parent's post-operation commit landed before the parker got there, and the child, which no replication server owns, then held the ServerState back for the rest of the class. Both tests now wait for the child to be seen parked before the parent is released - a parked change leaves by getNextUpdate() and by no other road, which is the hand-out they are about - and no longer assert which thread hands it out. The OpenIdentityPlatform#923 liveness assertion is made on the thread which met the Error. The javadoc of getNextUpdate() says what it guarantees.
|
@maximthomas could you take this one ahead of the queue? It is test-only - two assertions dropped, two waits added, one javadoc - one commit on master, What it is costingThe issue was filed on two hits (#981, #964, 2026-09-12). Since then the same assertion has taken down a leg on every day the replication PRs ran, on branches which touch nothing near it:
Ten legs in five days. Every one of them is the same shape - Two things make it worse than a flake to re-run. The reruns are what a reviewer sees first: a red What the PR doesNothing in Every day this waits is another row in that table. |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The diagnosis is right and the new wait pins what it claims.
getNextUpdate()never guaranteed the thread: the parker reachesLDAPReplicationDomain.replay()'s tail withdependency=true(thewhileat:2777skipped, thefinallyat:3147gating onlyprocessUpdateDone) and takes the child back itself when the commit landed first — the new javadoc atRemotePendingChanges.java:575-580matches the code clause by clause.- "Seen parked ⇒ off the queue" holds by construction:
dependentChangesis written only byaddDependency()fromcheckDependencies()on a dequeued message and drained only bygetNextUpdate()(andclear()on disable), andinitialDependent + 1is stable while the parent is held (the parker's owngetNextUpdate()returnsnullatRemotePendingChanges.java:644-648). - The Testing table shows the original assertions red 2/2 under a mutant the PR does not carry — the CI failure verbatim.
issue (blocking): The #923 liveness assertion is made on the thread which ran the by-hand redelivery, not on the thread which met the Error.
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java:3136, :3062-3068, :3120-3124
replayingChild is written by the ShortCircuitPlugin predicate on every replayed ADD carrying childCsn: errorFor() (ShortCircuitPlugin.java:851-863) evaluates matches.test(op) before the maxTimes budget check, so the side effect fires on the budget-spent replay too. The redelivery at :3120-3124 runs while thrown is still registered (deregister() is in the finally, after the assertion), so at :3136 the reference names the thread which just applied the child — alive by construction. Measured with the #923 mechanism removed (catch (Throwable t) → catch (Exception t) in ReplayThread.java:133): BASE red 1/1 on isAlive (:3118, the isSameAs at :3085 passed on that run; server log msgID=140 ... Replica replay thread 0 ... terminate abnormally ... LinkageError: the replay of this change is unwound once its ack is out), HEAD green 1/1 with every assertion held. The pin the description's third bullet and the javadoc at :3012-3014 describe is gone.
// UpdateOperationTest.java:3067 — the first replay of the child is the one which meets the Error;
// the redelivery must not overwrite it
replayingChild.compareAndSet(null, Thread.currentThread());Pin: the same mutant at HEAD, expected red 1/1 on :3136.
thought (non-blocking): A red exit of aChangeHandedOutAsADependencyIsGivenBackWhenItsReplayIsUnwound before the by-hand redelivery still wedges the class — the PR removes the #1036 trigger, not the cascade.
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java:3085, :3096-3102, :3139-3143, :3617-3632
On any red before the loop at :3115-3126 the finally runs thrown.deregister(); parked.deregister(): the parent is let through and commits, the child is handed out and applied, processUpdateDone() reads isAssured() (ackPublished = true), getCSN() throws regardless of the ShortCircuit registry, the child is given back uncommitted at the head of pendingChanges and nothing redelivers it — the 14-20 later cases time out as in #1036. The new wait is near-deterministic, so the likelier red exit stays the pre-existing thrownCount wait. Your call on scope; if wanted, a cover(childCsn)-guarded redelivery in the finally leaves the class clean whichever assertion went red.
suggestion (non-blocking): The queue arm is exercised by no measured run.
PR description, Testing table
The mutant delays getNextUpdate() callers only (parker 300 ms, others 500 ms) — the parker arm (child ADD REQ before parent ADD RES). The queue arm (parent RES before child REQ, the 2026-09-14 #981 job), which the dependent-changes-size wait is written for, was never seen converted into a parked child. One run would close it:
// LDAPReplicationDomain.replay(), before checkDependencies() — mutant, not for the PR:
// the child is dequeued, then sleeps past the parent's commit
Thread.sleep(500);Expected: original tests red (child replayed from the queue, isSameAs fails unless the parent's thread dequeues it), PR tests green, access log showing parent RES before child REQ only under the originals.
suggestion (non-blocking): The rule the PR refutes survives verbatim at three sites.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/RemotePendingChanges.java:93-94, :673-674; opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java:3000-3002
"the parked ones are handed to whichever thread clears what they wait for" (class javadoc), "a parked one is handed to the thread which clears what it waits for" (addDependency()), and the first paragraph of the second test's javadoc, ten lines above the paragraph the PR rewrote: "handed out by getNextUpdate() to the thread which cleared what it was waiting for, and that thread owns it from then on". The give-back conclusions stay true; the premise is the one the new getNextUpdate() javadoc drops.
// e.g. RemotePendingChanges.java:673-674
* parked one is handed to whichever thread calls getNextUpdate() first once the changes
* before it are gone - the clearing thread as a rule, the parker itself when the clearing
* landed first - and one which is not listed here anymore is gone with the pending changes
Fixes #1036.
Two tests of #958 -
aChangeHandedOutAsADependencyIsGivenBackWhenItsReplayIsUnwoundandtheChangesParkedBehindAChangeWhoseAckFailedAreReplayed- assertisSameAs(replayingParent):the child parked behind the parent must be replayed by the thread which committed the parent.
getNextUpdate()never guaranteed that. It hands a parked change to whichever replay thread callsit first once the changes before it have left
pendingChanges, and the thread which parked thechild calls it on its own way out, right after
addDependency(). The parent commits in thepost-operation plugin, inside
op.run(), so a parker delayed past the parent's write finds theparent gone and takes the child back itself. That is the 0.26 s failure on #981 and #964 - and,
because the assertion sits before the by-hand redelivery of a child no replication server owns,
the child then stays uncommitted at the head of
pendingChangesand the remaining 17 tests of theclass time out behind it.
The change
Test tree, plus one javadoc:
dependent-changes-size) before the parentis released. That is the property they are about - a parked change leaves by
getNextUpdate()and by no other road - and it closes a second hole the thread check hid: released on the spot,
the parent could commit before the child was taken off the queue at all, and the child would be
replayed from the queue with nothing to wait for, a pass which proves nothing.
why. The Replication: an Error in a replay kills a replay thread the pool never replaces #923 liveness assertion is made on the thread which met the Error, whichever one it was.
getNextUpdate()'s javadoc says what it guarantees: first caller once the changes before it aregone - as a rule the thread which cleared the dependency, but the parker itself when the clearing
landed before it got there.
Testing
getNextUpdate()300 ms late, every other caller 500 ms late - original testsReplica replay thread 1vsthread 0"to refer to the same object", the CI failure verbatim, second test cascadingUpdateOperationTestThe mutant is not part of the PR. The branch sits directly on
masterat 776339a, one commit.