Conversation
58df624 to
8f9a7fd
Compare
|
Force-pushed: rebased on The conflict was one hunk, the import block of What the rebase actually brought is worth a look, because it settles something this PR had only asserted. The last section of the description said the collapsed restarts of a It survives for a reason worth stating rather than leaving as a passed test. That test stops a replay thread while it holds a change and then reads the assured ack the abandoned delivery published. Under this change Run after the rebase, all green:
The two The description has been updated: the SHA to review is now 8f9a7fd, and the closing section says what became of the conflict it predicted. One thing unchanged by the rebase: this is still stacked on #958, which is itself conflicting with |
|
Ordinal moved: Nothing catches this on the way in. The additions land in different parts of the file, so git merges The open PRs which add to the file now hold 310-325 with nothing claimed twice:
No Java moved with it: the generated constant is the key name without its ordinal, so the rename is This branch is not one of the six: 316 was free where it sits, on the earlier [#922] commits, from |
31d7dfa to
0e91a8f
Compare
|
Force-pushed: rebased onto the current #958 at 31e633d (was 00fead7, the #958 of two review rounds ago), which sits on This time the conflict was not one of hunks. #958 went through two rounds since this was opened, and both reached into the code this PR changes, so what git reported in
The mechanical three: #901 removed One consequence for the description's "say plainly" paragraph: the precedent it named for the test hook, Run after the rebase, all green:
The description is updated: the SHAs, the bullets the two #958 rounds touched, and a closing section which says what was merged and how. |
0e91a8f to
ca9ba89
Compare
|
Force-pushed: rebased onto the current #958 at 9618f12 (was 31e633d), which sits on The two commits of this PR are what they were: 506 insertions and 67 deletions over the same five files. The one difference from the previous version is twelve lines of a comment #958 round 4 grew inside the What put both PRs back into conflict is worth a line, because it was not textual. #928 (#973) removed the NPE in Run after the rebase, all green:
The description is updated: the SHAs and the base. |
ca9ba89 to
823980c
Compare
|
Force-pushed: rebased onto The conflict GitHub reported was the squash, not the code. 776339a is #958's 9618f12 squashed onto Ordinal 325 is still claimed by nothing on Run on the new head: The description is updated: the stacking paragraph is gone, the SHAs and the base, and the closing section says what the squash did to the branch. |
823980c to
3592ab5
Compare
|
Force-pushed: rebased onto The conflict was one hunk, the tail of Nothing in the Java met #964: it changes About the red leg on the previous head (JDK 26): it was Run on the new head: The description is updated: the SHAs, the base, and the closing section says what #964 put in the file. |
3592ab5 to
2a2766f
Compare
|
Force-pushed: rebased onto The conflict was one hunk, the field block of Worth a line, since #974 reached into the same restart: it counts the generation inside Ordinal 325 is still claimed by nothing on Run on the new head, all green:
The description is updated: the SHAs, the base, and the closing section says what #974 did to the file. |
maximthomas
left a comment
There was a problem hiding this comment.
praise: SessionRestartRequests is the right shape for this: the request outlives the thread which made it, the merge keeps the strongest level, and take-before-run is kept and argued — the duplicate-delivery reason is the one that matters. Giving the request back after a throw and letting the state checkpointer run what a leaving thread cannot is a clean choice: one thread per domain, already waited for by shutdown(), no new thread to leak. The backoff is waited on a monitor outside serviceStateLock, and disable()/shutdown() wake it, so a domain going away never sits out a backoff it is not owed. shutdown() waits for the checkpointer outside the lock, so a restart on that thread cannot deadlock it. The description is precise where the issue text was not (enableService() unguarded vs the broker's own reconnect loop), the mutant table is the kind of evidence a reviewer can re-run, and the #958 "merge of decisions" section made the rebase auditable. SessionRestartRequestsTest reads as a spec.
issue (blocking): the checkpointer's own failure handling is pinned by no test
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java:2407
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3673-3680
failNextSessionRestarts(1) is spent by the replay thread's own restart; the give-back is pinned, but the checkpointer's catch (Throwable) and ERR_REPLAY_SESSION_RESTART_FAILED_325 are entered by no test. Measured: catch → rethrow in runPendingSessionRestart() leaves aSessionRestartWhichCouldNotRunIsRunAgain green (1 run / 0 failures). Under that mutant a second refusal ends the checkpointer — no more ServerState saves, and shutdown() spins in while (!done).
Two failures reach the catch once; the report comes out exactly once:
domain.failNextSessionRestarts(2);
final CSN csn = gen.newCSN();
final List<String> records = errorLogRecordsOf(() -> {
broker.publish(new DeleteMsg(tmp.getName(), csn, uuid));
assertNull(getEntry(tmp.getName(), 60000, false),
"the change was not delivered again after the session restarts which failed");
return null;
});
assertEquals(countRecordsOf(records,
"Could not restart the replication session of domain \"" + baseDN + "\""), 1,
"the checkpointer reports the restart which threw on it, once, and runs it again");
assertEquals(domain.getSessionRestartFailuresLeft(), 0, "...");Plus a third row in the description's mutant table: runPendingSessionRestart() catch → rethrow: red (the change is never delivered; the checkpointer is gone).
question (blocking): is the ieRunning() gate on the checkpointer road deliberate?
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3664
ieRunning() covers exports; ownsItsSession() (shutdown || disabled) covers only a total update into this replica. At base, abandonReplay() ran the restart at once, export or not. Now a change abandoned by a thread-count change during an export is redelivered only once the export ends (minutes on a large backend), with the ServerState stopped behind it — while the replay thread's own restart (:3596) and the last resort in replay() (:2629) still restart mid-export. Neither the description nor the javadoc names the gate.
If deliberate (a restart mid-export unwinds the export the session carries): one line in the description and in the javadoc, and the two synchronous roads as a follow-up. If not: drop it, or gate all three.
issue (non-blocking): the report inside the checkpointer's catch is unguarded
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3679
The catch says "It must not end on it", then formats a stack trace — on the OOME road the catch exists for. A throw out of the report escapes, the checkpointer ends, shutdown() spins in while (!done) (:2524; done is set at :668 with no finally). replay() guards its own report at :2662-2672 for exactly this reason; same shape here:
catch (Throwable t)
{
try
{
logger.error(ERR_REPLAY_SESSION_RESTART_FAILED, getBaseDN(), stackTraceToSingleLineString(t));
}
catch (Throwable reportFailure)
{
// The restart is asked for again already; the report must not end this thread.
}
}(Overlaps #977; the guard belongs here whichever lands first.)
suggestion (non-blocking): clear the holder in enable() too
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:4519, :4646
disable() clears the holder after the drain; enable() never does. A request landing after the clear (a queued change of this domain dequeued after awaitReplayDrained() and abandoned) survives the disabled span, and the checkpointer restarts the fresh session ≤ 1 s after enable(). Harmless — the generation guard keeps it to one restart — but one reconnect nobody asked for. A sessionRestarts.clear() under the lock at the top of enable() closes it.
suggestion (non-blocking): the backoff wait and its two wakes are reached by no test
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3784-3792, :4524, :2504
No case disables or shuts a domain down while a restart sits in its backoff, so Thread.sleep(left) in place of the monitor wait survives by construction. A case which asks for a restart with failNextSessionRestarts(1), calls domain.disable() during the backoff and bounds its duration well under MAX_REPLAY_RETRY_DELAY_IN_MS would pin both the wait and the wake. If that is out of scope, say so in the mutant table.
suggestion (non-blocking): pin the give-back level, and say what giveBack() takes
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/SessionRestartRequests.java:88-92, LDAPReplicationDomain.java:3637
@param taken what take() returned to the caller which could not run it — the one caller passes the constant AFTER_BACKOFF, and the class javadoc's "must not spend the wait another thread's request was made with" does not say which thread is which. The unit test gives back the level it took, so a NOW take given back as AFTER_BACKOFF is pinned nowhere:
@Test
public void aRestartGivenBackAfterItThrewIsOwedTheBackoff()
{
final SessionRestartRequests requests = new SessionRestartRequests();
requests.request(NOW);
assertEquals(requests.take(), NOW);
requests.giveBack(AFTER_BACKOFF);
assertEquals(requests.take(), AFTER_BACKOFF,
"a session which could not be started is what the wait exists for");
}This pins the merge; the production constant stays pinned only through the backoff case above.
…y its restart waits out a total update, and wake its backoff Review round 1 of OpenIdentityPlatform#981. The catch of the state checkpointer was entered by no test: the one injected failure was spent by the replay thread's restart, and the checkpointer's ran. aSessionRestartWhichCouldNotRunIsRunAgain now fails two restarts, and asserts that the checkpointer reports the one which threw on it exactly once before the change is delivered; a catch which rethrows is red. The report inside that catch is guarded the way the give-back's report in replay() is: on the road out of a JVM which has refused an allocation, building the line is a throw of its own, and it would end the thread shutdown() waits for. The ieRunning() gate of runPendingSessionRestart() is said out loud. A restart stops the session a total update runs over, in either direction: an import into this replica ends on the entries which had arrived - and ownsItsSession() does not see an online total update at all, since preBackendImport() keeps this domain's own backend events from disabling it - and an export is given up by exportLDIFEntry() on the broker stop. The checkpointer can wait: the request stands until the context is released. enable() clears the holder as disable() does. A request made between a replay thread's read of the flag and disable()'s clear survived the disabled span, and the checkpointer restarted the session enable() had just started, within the second, for a change gone with the pending changes. giveBack() takes what the caller asks for again, not what take() returned, and says so; a NOW take given back as AFTER_BACKOFF is pinned. SessionRestartBackoffTest pins the monitor wait and both its wakes on a domain of its own: shutdown() during the backoff returns in a fraction of it, and after disable() / enable() the checkpointer saves a change within its next tick. Each wake is caught by its own case.
|
Round 1 is 916afd5, one commit on top of the two: 381 insertions and 15 deletions over five files, Blocking 1 - the checkpointer's Blocking 2 - the The unguarded report. Guarded, in the shape of the give-back's report in The holder cleared in The backoff wait and its wakes. Pinned rather than declared out of scope: The give-back level. Run on the new head, one JVM per class: |
…y its restart waits out a total update, and wake its backoff Review round 1 of OpenIdentityPlatform#981. The catch of the state checkpointer was entered by no test: the one injected failure was spent by the replay thread's restart, and the checkpointer's ran. aSessionRestartWhichCouldNotRunIsRunAgain now fails two restarts, and asserts that the checkpointer reports the one which threw on it exactly once before the change is delivered; a catch which rethrows is red. The report inside that catch is guarded the way the give-back's report in replay() is: on the road out of a JVM which has refused an allocation, building the line is a throw of its own, and it would end the thread shutdown() waits for. The ieRunning() gate of runPendingSessionRestart() is said out loud. A restart stops the session a total update runs over, in either direction: an import into this replica ends on the entries which had arrived - and ownsItsSession() does not see an online total update at all, since preBackendImport() keeps this domain's own backend events from disabling it - and an export is given up by exportLDIFEntry() on the broker stop. The checkpointer can wait: the request stands until the context is released. enable() clears the holder as disable() does. A request made between a replay thread's read of the flag and disable()'s clear survived the disabled span, and the checkpointer restarted the session enable() had just started, within the second, for a change gone with the pending changes. giveBack() takes what the caller asks for again, not what take() returned, and says so; a NOW take given back as AFTER_BACKOFF is pinned. SessionRestartBackoffTest pins the monitor wait and both its wakes on a domain of its own: shutdown() during the backoff returns in a fraction of it, and after disable() / enable() the checkpointer saves a change within its next tick. Each wake is caught by its own case.
916afd5 to
c2455f8
Compare
|
Force-pushed: rebased onto Git reported nothing, and the merge did not compile - which is what every CI leg of the previous head failed on, in #968 resets the restart request at the end of The two landings met this branch elsewhere as well, without a hunk between them:
Ordinals: #968 took 322, #977 took 323 and 324, this branch keeps 325; the file has no ordinal twice. Run on the new head, one JVM per class, all green: The description is updated: the SHAs, the base, the mutant row, and a closing section on the two landings. |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The rebase and the round-1 fixes hold up to measurement, not just to reading.
- The rebase is byte for byte: the
-U0line sets of the three commits match the previous head, the only residue being theimportBackend()clear (sessionRestarts.clear(),LDAPReplicationDomain.java:5474) and one comment. - The round-1 blocking issue is closed and pinned: with
failNextSessionRestarts(2)the second failure is the checkpointer's, and the catch→rethrow mutant inrunPendingSessionRestart()dies at the 60 s delivery assert (UpdateOperationTest.java:2421; the checkpointer dead with msgID 140, the JVM exiting through #977's join). SessionRestartBackoffTest's shutdown case pins the in-monitor sleep mutant: "shutdown() waited 2783 ms" here, 2812 ms in your run.- The guarded 325 report, the clear in
enable()under the lock, the wakes ordered after the flags in bothdisable()andshutdown(), and the wait outsideserviceStateLockall read correct; theowned == nullrouting at:3718makes the count of one inUpdateOperationTestthe only road for the injected failures. - Ordinal 325 is unique against
origin/master; all five Linux cells and CodeQL are green.
issue (blocking): The disable case does not time disable(), so a checkpointer which sits out the backoff while holding the monitor leaves it green.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartBackoffTest.java:147-148, :166-174; opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:4824, :4830-4832
The wake is the last thing disable() does, and it is a synchronized (sessionRestartBackoff) block. With Thread.sleep(left) in place of sessionRestartBackoff.wait(left) at :3990 the sleeping checkpointer holds that monitor, disable() blocks at :4830 for the rest of the backoff and returns only once the checkpointer is out of it; enable(), the modify and the 2.5 s clock all start after the stall, the save lands a tick later, and the case is green — measured 1/1 (9.4 s against 8.3 s at head), while the shutdown case goes red on the same mutant. The table's "Thread.sleep() in place of the wait: both are red" holds only for a sleep outside the monitor; the description does not say which shape was run. Time disable() the way the shutdown case times shutdown():
final long started = System.nanoTime();
domain.disable();
final long tookMs = NANOSECONDS.toMillis(System.nanoTime() - started);
assertTrue(tookMs < SHUTDOWN_BOUND_IN_MS, "disable() waited " + tookMs
+ " ms: it sat out the backoff of the session restart the state checkpointer"
+ " was waiting through, for a session disable() was cutting anyway");
domain.enable();(SHUTDOWN_BOUND_IN_MS then bounds both wakes; rename it.) With this the case is red under either sleep shape. Please also name the shape in the table.
issue (non-blocking): UpdateOperationTest asserts the 325 report exactly once while its own comment names the outcome of two as legitimate.
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java:2427-2436
If the checkpointer's tick lands between the replay thread's request() (LDAPReplicationDomain.java:3763) and its CAS (:3780), the checkpointer spends both injected failures and logs 325 twice; the case is then red on the road the comment describes. The window is sub-microsecond, so this is a rare flake, not a wrong pin — a count of 0 has no road (replayFailed(csn) at :3718 nulls the owner, so replay()'s catch never reports), and the count still pins the report.
final int reported = countRecordsOf(records,
"Could not restart the replication session of domain \"" + baseDN + "\"");
assertTrue(reported == 1 || reported == 2, "the state checkpointer reports the restart"
+ " which threw on it once, or twice when its tick took the replay thread's request"
+ " as well: " + reported + " in " + records);issue (non-blocking): The wake in disable() is not sticky, and the test's back-to-back enable() can put the checkpointer back into the backoff.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3984-3991, :4800, :4824, :4987; opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartBackoffTest.java:147-148
The wake is flag-then-notify. A checkpointer descheduled between notifyAll() and its loop re-check for longer than enable() takes to reach disabled = false (:4987, after the clear and loadDataState()) re-evaluates !disabled as true and waits the rest of the backoff (~2.6 s in the test); the save then lands ~3.6 s after disable(), past CHECKPOINT_BOUND_IN_MS — the false red the class javadoc says the case never reports. In production the cost is latency only (block 2 returns on the generation). Not seen in 1/1 here or 2/2 in your runs; the mechanism is certain from the code. A wake generation makes it sticky:
/** Bumped under {@link #sessionRestartBackoff} on every wake, so that a wake is never missed. */
private long sessionRestartBackoffWakes;
private void wakeSessionRestartBackoff()
{
synchronized (sessionRestartBackoff)
{
sessionRestartBackoffWakes++;
sessionRestartBackoff.notifyAll();
}
}
// in waitBeforeSessionRestart()
synchronized (sessionRestartBackoff)
{
final long wakes = sessionRestartBackoffWakes;
for (long left = until - monotonicNowInMs();
left > 0 && !shutdown.get() && !disabled && wakes == sessionRestartBackoffWakes;
left = until - monotonicNowInMs())
{
sessionRestartBackoff.wait(left);
}
}issue (non-blocking): The two holder clears outside the checkpointer road — enable() and importBackend() — are pinned by nothing.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:4960, :5474
The disable case runs disable() (which clears at :4819) before enable(), and UpdateOperationTest never disables, so deleting :4960 survives by construction, as the description says; :5474 is reached by no test of this PR and has no table row. A pin needs a request standing while the domain is disabled, which the existing hooks can observe once a request hook exists (the precedent of failNextSessionRestarts):
domain.disable();
domain.requestSessionRestart(); // @VisibleForTesting: sessionRestarts.request(SessionRestart.NOW)
domain.failNextSessionRestarts(1);
domain.enable();
Thread.sleep(2000); // two checkpointer ticks
assertEquals(domain.getSessionRestartFailuresLeft(), 1,
"the request which stood while the domain was disabled was run against the session enable() started");
assertTrue(domain.isConnected());With :4960 deleted the checkpointer runs the leftover request within the second, spends the injected failure and leaves the session down. Or: record both clears as unpinned in the table — and say whether #968's tests reach :5474.
issue (non-blocking): The giveBack(AFTER_BACKOFF) call site is pinned by the holder's unit case only; giveBack(restart) survives every case.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:3804; opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartRequestsTest.java:86-96
aRestartGivenBackAfterItThrewIsOwedTheBackoff pins the merge inside the holder; on the road both integration cases take the level taken is already AFTER_BACKOFF, so giving the taken level back is the same thing. Only a NOW take whose restart throws — a replay abandoned past its give-up budget, abandonReplay() — tells the two apart, and no case reaches it (the description says so). Either a case on that road asserting isConnected() is still false 1.2 s after the throw, as leaveTheCheckpointerInTheBackoff does, or record the call site as unpinned in the table.
suggestion (non-blocking): startDomain() asserts before the caller's assignment, and the finally runs its three steps unchained — a failed setup or a throwing release() leaks the domain or the RS into the next case.
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartBackoffTest.java:105, :141, :178-184, :268-277
createNewDomain() + start() + assertTrue(isConnected()) all run before domain = is assigned; a failed assert leaves a started, registered domain that finally never deletes (domain == null; createNewDomain() replaces it silently in the map). Assign before the assert, as SessionRestartTest does, and chain the cleanup:
domain = startDomain(baseDN, rsPort); // startDomain(): create + start(), no assert
assertTrue(domain.isConnected(), "the domain did not connect to its replication server");
...
finally
{
try
{
release(domain, broker);
}
finally
{
try
{
if (domain != null)
{
MultimasterReplication.deleteDomain(baseDN);
}
}
finally
{
remove(replicationServer);
}
}
}question (non-blocking): Were the wake-removal rows of the mutant table run at this head?
The two rows ("removed from disable() alone: the disable case red"; "from shutdown() alone: the shutdown case red") were not run this round; the three runs went to the head, the rethrow mutant and the sleep mutant. If they were run, a word on it in the description is enough.
nitpick (non-blocking): "A restart which throws is given back and run by the checkpointer instead of going out with the thread" is not true on the OOME/last-resort road of replay()'s catch, and the comment there is now stale.
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java:2731-2735; the PR description
On that road (owned != null, recoverFromReplayFailure(owned, replayThreadShutdown, true)) the thread still re-runs the merged request itself after 317, as at base — what changed is that the re-run now carries the backoff. So the comment "the session is restarted without sitting through the backoff, since the thread which is doing it is on its way out" is false at this head, and the description's sentence needs one clause: "except the OOME road of replay(), which still restarts in-thread, now with the backoff". 317's text ("the session is being restarted so that the change is delivered again") names a restart the give-back's success does not imply; ride-along if touched.
… has run The recovery from a failed replay takes the request before it runs the restart, and restartSession() stops the session in its first synchronized block and starts it again in the second. Anything thrown in between left the request cleared and the session stopped, and nothing asked for it again: no change is delivered over a session which is down, so no replay fails and no thread comes back to the recovery. The domain sat out of the topology - the change it could not replay still owned by the replication server, its ServerState stopped behind it - until the server was restarted or a configuration change happened to restart the service. The throw is narrow but not theoretical: enableService() is the one call on that road nothing guards, while the reconnect loop of the broker wraps the same connectAsDataServer() in a catch, so every failure that path is written to survive is fatal to this one. The request is a SessionRestartRequests now. It is still taken before the restart runs, as it must be - a change released while a restart is under way is not one that restart asks for - and it is given back when the restart could not run, with the backoff whatever it was asked for with, since a session which can not be started is what that wait exists for. The wait belongs to the request rather than to the thread which runs it: a request made by a replay thread on its way out is owed none, one made by a failing replay is owed one, and either thread can end up running the other's request. Nothing else runs a request which is left standing, so the state checkpointer of the domain does, once a second: it is the one thread a domain has for as long as it is up, whatever its session is doing. That also lets abandonReplay() ask for the restart rather than run it - the threads of the pool are stopped one after the other and joined, so a live num-update-replay-threads change cost one stop and reconnect per thread which was replaying a change, none of them waiting, while the configuration change waited for all of them - and it runs the bare hand-back of issue OpenIdentityPlatform#922, which asked for a restart nothing was going to run. The backoff is waited on a monitor rather than slept through, and a domain which is going away or is being disabled wakes it: the thread which holds it is a replay thread the shutdown of the pool joins, or the checkpointer the shutdown of the domain waits for.
…nothing else claims
…y its restart waits out a total update, and wake its backoff Review round 1 of OpenIdentityPlatform#981. The catch of the state checkpointer was entered by no test: the one injected failure was spent by the replay thread's restart, and the checkpointer's ran. aSessionRestartWhichCouldNotRunIsRunAgain now fails two restarts, and asserts that the checkpointer reports the one which threw on it exactly once before the change is delivered; a catch which rethrows is red. The report inside that catch is guarded the way the give-back's report in replay() is: on the road out of a JVM which has refused an allocation, building the line is a throw of its own, and it would end the thread shutdown() waits for. The ieRunning() gate of runPendingSessionRestart() is said out loud. A restart stops the session a total update runs over, in either direction: an import into this replica ends on the entries which had arrived - and ownsItsSession() does not see an online total update at all, since preBackendImport() keeps this domain's own backend events from disabling it - and an export is given up by exportLDIFEntry() on the broker stop. The checkpointer can wait: the request stands until the context is released. enable() clears the holder as disable() does. A request made between a replay thread's read of the flag and disable()'s clear survived the disabled span, and the checkpointer restarted the session enable() had just started, within the second, for a change gone with the pending changes. giveBack() takes what the caller asks for again, not what take() returned, and says so; a NOW take given back as AFTER_BACKOFF is pinned. SessionRestartBackoffTest pins the monitor wait and both its wakes on a domain of its own: shutdown() during the backoff returns in a fraction of it, and after disable() / enable() the checkpointer saves a change within its next tick. Each wake is caught by its own case.
…count the wakes, and pin the clears and the give-back level Review round 2. - SessionRestartBackoffTest times disable() as it times shutdown(), against the one bound both wakes share: a checkpointer which sleeps the backoff through while it holds the monitor holds disable() for the rest of it, and the disable case was green under that shape of the sleep mutant. Both shapes are in the mutant table now. - The wake of the backoff is counted as well as flagged, and the count is read where the session is stopped, under serviceStateLock: `disabled` does not stay set, so a checkpointer descheduled between disable()'s notifyAll() and its own re-check for as long as enable() takes would wait out the rest of the backoff for a session it has nothing left to start. - aSessionRestartWhichCouldNotRunIsRunAgain accepts the checkpointer's report twice, on the road its own comment names: its tick landing between the replay thread's request and the CAS. - requestSessionRestart() (@VisibleForTesting) leaves a request standing, and two cases pin the clears in enable() and importBackend() on it - the second in OpenIdentityPlatform#968's ReplayDuringImportTest, which has the import fixture. - aRestartAskedForWithoutTheBackoffIsGivenBackWithIt pins the level a restart which could not run is given back with, on the abandonReplay() road OpenIdentityPlatform#941's fixture reaches: a NOW request given back as taken has the session back a second early. - startDomain() asserts nothing, so that a case which fails on its way up still holds the domain to delete, and the cleanup chains its three steps. - The comment on the OOME road of replay() and ERR_REPLAY_GIVE_BACK_FAILED say what happens: the restart is asked for, and the last resort re-runs a given-back request on the same thread, backoff and all.
c2455f8 to
c659a42
Compare
|
Round 2 is c659a42, one commit on top of the three: 374 insertions and 39 deletions over five files, Blocking - the disable case does not time The count of 325. The wake is not sticky. Counted, as you sketched, with one difference in where the count is read: The two holder clears. Pinned, both, on a The give-back level. Pinned on the road you named:
The wake-removal rows. They were not run at c2455f8: they were run on 916afd5, the head of round 1 before the rebase, whose round-1 commit the rebase left byte for byte and whose first commit it changed by the The OOME road. As you read it: the first run of the restart on that road is without the backoff, and one which throws is given back with it and re-run by the last resort of Run on c659a42, one JVM per class, all green: the four classes this round touches - Rebased onto |
Fixes #925.
Was stacked on #958, which has landed (776339a). The branch now sits on
masterdirectly - at 600df92, which is #950 (#978) - and the diff is four commits: 03e9cd3 ([#925] Keep asking for a session restart until it has run), df61f75, which moves its message ordinal, 82eda0e, review round 1, and c659a42, review round 2 (the last section). The rounds #958 went through while this was open reached into the same code, so the rebases along the way were a merge of decisions rather than of hunks - the last section says which, and it still describes how this change meets what #958 put onmaster.What was wrong
runRequestedSessionRestarts()takes the request before it runs the restart, andrestartSession()stops the session in its first synchronized block and starts it again in the second. Anything thrown in between left the request cleared and the session stopped, and nothing asked for it again: no change is delivered over a session which is down, so no replay fails and no thread comes back to the recovery. The domain sat out of the topology - the change it could not replay still owned by the replication server, its ServerState stopped behind it - until the server was restarted or a configuration change happened to callrestartService().Worth being precise about the trigger, because the issue text names one which does not happen:
broker.start()on an unreachable replication server does not throw.connectAsDataServer()swallows every connection failure -performPhaseOneHandshake()catchesConnectException,SocketTimeoutExceptionandException,connectToReplicationServer()catchesException- and leaves the domain in degraded mode, where the listener thread reconnects on its own. What makes the throw reachable is narrower and, in one respect, damning:enableService()is the one call on that road nothing guards, while the reconnect loop of the broker wraps the sameconnectAsDataServer()in acatch (Exception). Every failure that path is written to survive is fatal to this one, and what is fatal about it is the missing listener thread: it is the reconnection engine, so a domain which loses it has nothing left to bring the session back.What changed
SessionRestartRequests(new) holds what the domain has been asked for -NONE<NOW<AFTER_BACKOFF, merged by taking the strongest. The request is still taken before the restart runs, which it must be: a change released while a restart is under way is not one that restart asks for, its delivery would be turned down as a duplicate of a change a replay thread still owns. Clearing the flag after the restart instead - the other half of the suggestion in the issue - would swallow exactly those requests.NOWrequest given back bare would have the retry hammer once a second. [#922] Give a change back when the replay which owns it is unwound #958 has since put the request back on its own (round 2, blocking 4) - afinallywhich sets the flag again - and this replaces it: the give-back carries the backoff, and, the point of this PR, something runs what was given back. Under [#922] Give a change back when the replay which owns it is unwound #958 alone the restored request is run by "the next failed or abandoned replay of this domain", and on a session which is down there is none.stopReplayThreads()sets the shutdown flag of each thread in a loop, so the window is the one between the first flag and the last. It costs nothing to close it here, since the request had to carry its own state anyway. The thread anOutOfMemoryErroris ending, which [#922] Give a change back when the replay which owns it is unwound #958 exempts from the backoff for the same reason, asks forNOWas well.shutdown()takes that monitor to wake it - and swallows and reports a failure rather than end on it: a domain whose checkpointer is gone saves no ServerState, andshutdown()waits for that thread. The report itself is guarded the way the report of a give-back which failed is: on the road out of a JVM which has refused an allocation, building the line is a throw of its own, and it must not end the thread either.ieRunning()). A restart stops the session the total update runs over. An import into this replica reads its entries from that session and would end on the ones which had arrived - anddisableddoes not say such an import is running, sincepreBackendImport()keeps this domain's own backend events from disabling it, soownsItsSession()does not see it. An export from this replica publishes its entries over it, andexportLDIFEntry()gives the export up asERR_INIT_RS_DISCONNECTION_DURING_EXPORTonce the broker has been stopped under it, which leaves the replica it was initializing to be initialized again. The checkpointer is the thread which can afford to wait: the request stays standing, it ticks once a second, and the restart runs as soon as the context is released; the change waits for as long as the total update takes, and the ServerState with it, while the replay keeps running. The two roads which restart synchronously - the replay thread's own restart and the last resort ofreplay()- still restart mid-export, as they do onmaster: gating them the same way is only safe once something runs what they leave standing, which is this change, and is left to Replication: a session restart run by the thread which released a change cuts the export the session carries #1048. The import direction of those two roads is A failed entryUUID search reads as a deleted entry, and conflict resolution records the change as replayed #956 ([#956] Tell a failed entryUUID search apart from an entry which is not there #968).abandonReplay()asks for the restart rather than running it (the second asymmetry). The threads of the pool are stopped one after the other and joined, so a liveds-cfg-num-update-replay-threadschange cost one stop and reconnect per thread which was replaying a change - none of them waiting, each one a listener join plus a full handshake, and the configuration change waited for all of them. One restart is all the replication server needs to send back every change which was handed back.replay()keeps running its restart itself, as [#922] Give a change back when the replay which owns it is unwound #958 decided in its round 1 (non-blocking 4): on that road the thread is likely ending and the report is the line an operator acts on. What this branch adds there is the same as everywhere else - a restart which throws leaves its request standing, given back with the backoff - except that on this road the last resort itself runs what is standing, on the same thread and backoff and all, so the first run of the restart is the only one without the wait; only a restart which throws there as well is left to the checkpointer, where before the request went out with the thread.disable()/shutdown()wake it. This is not in the issue; it is what the checkpointer needs in order not to hold a domain going down for up to ten seconds, and it removes the same stall for a replay threadstopReplayThreads()is joining. The wake is counted as well as flagged:disableddoes not stay set, so a checkpointer descheduled betweendisable()'snotifyAll()and its own re-check for as long asenable()takes to clear the flag would find nothing to wake for and wait out the rest of the backoff, for a session the restart has nothing left to start. The count is taken where the session is stopped, underserviceStateLock, so adisable()which has yet to take that lock is one the wait sees whether it is under way yet or not.enable()clears the holder, asdisable()does. A request made in the window between a replay thread's read ofdisabledanddisable()'s own clear -abandonReplay()reads the flag, logs, then asks - would survive the disabled span, and the checkpointer would restart the sessionenable()just started, within the second, for a change which is gone with the pending changes. Every request standing atenable()is that one.Tests
SessionRestartRequestsTest(new)NOWtake given back asAFTER_BACKOFFis owed the backoffUpdateOperationTest.aSessionRestartWhichCouldNotRunIsRunAgain(new)UpdateOperationTest.aRestartAskedForWithoutTheBackoffIsGivenBackWithIt(new)SessionRestartBackoffTest(new)shutdown()anddisable()each return in a fraction of it, and afterdisable()/enable()the checkpointer saves a change of the replica's own within its next tick; and a request left standing across adisable()/enable()is not run against the sessionenable()startedReplayDuringImportTest.aRequestWhichStoodWhileTheImportRanIsNotRunOnceItIsOver(new, in #968's class)UpdateOperationTest/ReplayDuringImportTestmasterplus the ones aboveRemotePendingChangesTest/AssuredReplicationPluginTest/DependencyTestDisabledDomainServerStateTest/SessionRestartTest/LDAPReplicationDomainConfigChangeTestenable()changeThe tests were checked against the mutations they exist for, each mutant compiled in place of the class and run by itself:
runPendingSessionRestart()removedaSessionRestartWhichCouldNotRunIsRunAgainred: the change is never deliveredcatchrethrowsmastersince #977 itsshutdown()no longer waits for a thread which has ended, so what is lost is the checkpointing of the domain and the restart nobody else runs, not its shutdown - the test JVM ends by itself, in 72 sThread.sleep()in place of the monitor wait, inside the monitor - the checkpointer holds it through the sleepdisable()case red ondisable()(2804 ms), theshutdown()case red onshutdown()(2764 ms): each is held by the wake it gives, which takes the monitorThread.sleep()in place of the monitor wait, outside the monitor - the whole synchronized loop replacedshutdown()case red onshutdown()(2786 ms), thedisable()case red on the checkpoint, which did not come within 2500 ms: the wake reaches nothingdisable()disable()case red on the checkpoint (2500 ms), theshutdown()case greenshutdown()shutdown()case red (2806 ms), thedisable()case greennotifyAll()and its re-check for as long asenable()takes, which no test can hit on purpose. Closed by construction, unpinnedenable()removedaRequestWhichStoodWhileTheDomainWasDisabledIsNotRunOnceItIsEnabledBackred: the failure was spent, the request was run against the sessionenable()startedimportBackend()removedaRequestWhichStoodWhileTheImportRanIsNotRunOnceItIsOverred: the failure was spent, the request was run against the session started back at the end of the importgiveBack(restart)in place ofgiveBack(AFTER_BACKOFF)- the request given back as takenaRestartAskedForWithoutTheBackoffIsGivenBackWithItred: the session was started back within 1500 ms of the restart which threw.aSessionRestartWhichCouldNotRunIsRunAgainand the three cases ofSessionRestartBackoffTestgreen, as they takeAFTER_BACKOFFalreadyEvery row was run at this head, one JVM per run, with the mutant compiled in place of the class.
The backoff test acts on the domain by the clock: the checkpointer takes the request back one second after it reported the failure - its
wait(1000)- and holds the backoff for three, and the test acts a fifth of a second into those three. A machine slow enough to push the checkpointer's tick past that delay has the test act before the backoff begins, which a slept-through wait survives: the test then proves less, but reports nothing false. The give-back case inUpdateOperationTestis of the same kind: it looks at the session 1.5 s after the throw, halfway between the second the checkpointer's next tick is away and the further second the backoff adds, and a tick pushed past that delay has it look before the session could be back under either level.Two things to say plainly rather than bury.
failNextSessionRestarts(int)/getSessionRestartFailuresLeft()/requestSessionRestart()are@VisibleForTestingand there is a branch inrestartSession()which production never takes; the tests of the checkpointer stand on the first two, and the two which pin the clears inenable()andimportBackend()on the third, since a request which stands across a disabled span or an import is made in a window no test can hit on purpose. Nothing else reachesenableService(): connection failures are swallowed inside it, and the machinery #958 added throws from a replay, not from a restart. The precedent this PR first named,setReplayGiveUpDelay(), is gone - #901 replaced it with the configuredreplay-give-up-delay- so the one left in the class issetReplayDrainTimeout()from #945. If you would rather have no injection point in the class, the alternative I can see is dropping the integration test and keeping only the unit test of the holder, which leaves the give-back and the checkpointer unpinned.The collapsed restarts of a
num-update-replay-threadschange are half pinned.aRestartAskedForWithoutTheBackoffIsGivenBackWithItstops one replay thread while it holds a change and reads the restart the checkpointer runs for it, so "the thread asks rather than runs, and the checkpointer runs it" is pinned; "one restart rather than one per thread" is not, since that needs as many parked threads as changes, and the fixture parks one.#941 has since landed (d0422c6), and
aChangeAStoppedReplayThreadHeldIsGivenBackAndDeliveredAgainsits in this class next to the change it was claimed to survive, and it is green. It is worth saying why it survives rather than only that it does. That test stops a replay thread while it holds a change and reads the assured ack the abandoned delivery published; under this changeabandonReplay()no longer restarts the session itself, so the ack now goes out over a session which is still up, where before it raced the teardown. The test is not merely still valid - it is the less brittle for this change. Its comment on reading the ack rather than its ordering ("an ack published after the hand-back reaches it all the same") describes a race this change removes.Rebased onto #958 as it went, and onto
masteronce it landedTwo rounds of #958 landed on the same code between the opening of this PR and now, and git reported six hunks in
LDAPReplicationDomain. Three were mechanical - #901 removed thereplayGiveUpDelayInMsfield and its setters this branch had sat next to, so they go; theoutOfMemoryparameter #958 gaverecoverFromReplayFailure()becomes the request beingNOW. Three were decisions:finallyaroundrestartSession()which sets the flag again. Replaced bygiveBack(AFTER_BACKOFF), for the two reasons above: the backoff travels with the request, and the checkpointer runs it. The concrete trigger that round named - the operating system refusing the listener thread - is kept in the comment.replay()run the restart rather than leave a request. Kept as decided there; the request is madeNOWfirst, so a restart which throws is given back and run by the checkpointer instead of going out with the thread. The earlier text of this PR, which said that road "asked for a restart nothing was going to run", described the [#922] Give a change back when the replay which owns it is unwound #958 of the time and is gone.ERR_ACK_NOT_PUBLISHED_316and the 316 this branch first took met in one hunk, and git did report it this time. 0e91a8f resolves it toERR_REPLAY_SESSION_RESTART_FAILED_325; the file has no ordinal twice. The open PRs which add toreplication.propertiesstill hold 310-325 with nothing claimed twice.The diff of the two commits against
masteris 506 insertions and 67 deletions over the same five files as before.Rebased once more onto #958 round 4 (9618f12, on
masterat 13d57e0). Round 4 only grew the comment inside thefinallythis branch replaces, so the resolution above stands and those lines go with the block; the twelve of them are the whole difference from the previous version of the two commits. What put both PRs back into conflict was #928 (#973), which removed the NPE #958's test scaffolding relied on to fail a replay before the CSN is read - #958's own rebase moved that scaffolding toModifyMsgWhoseOperationRefusesAControl, and this branch sits on it as it is.#958 has since landed as 776339a, a squash of the 9618f12 this branch sat on, with the same tree. Rebased onto
masterthere: the five commits it carried went, the two of this PR moved without a hunk to resolve, and the tree of the new head is the tree of the old one -git diff ca9ba89 823980cis empty.Rebased once more onto
masterat 9ff409b, where #964 landed. It tookWARN_CHANGELOG_READ_AGAIN_FOR_MISSING_CHANGES_321at the end ofreplication.properties, next to the message this branch adds, and that was the whole conflict: one hunk, both sides kept, 321 above 325. Nothing in the Java met #964 - it changes the replication server's catch-up, this branch changes the domain's restart - and the two commits are what they were, 506 insertions and 67 deletions over the same five files. 325 is claimed by nothing onmaster, which holds 310-317, 319-321 and 326-327; the file has no ordinal twice.Rebased once more onto
masterat cebef54, where #926 (#974) landed. It movedserviceStateLockandsessionGenerationup fromLDAPReplicationDomainintoReplicationDomain, and this branch addssessionRestartFailuresToInjectright where they used to be - one hunk, the field block, resolved by takingmaster's side and keeping only the new field. Everything else merged without a word, and arange-diffagainst the previous version of the two commits shows context only: the moved declarations, and thesessionGeneration++#974 took out ofrestartSession()next tofailSessionRestartIfATestAskedFor(). #974 reached into the same restart, so the meeting point is worth a line: it counts the generation insideenableService(), and only once the session is up, so a start which threw leaves the generation where the stop put it. The injected failure of this branch sits between the generation guard andenableService(), which is the same case - the thread which stopped the session still owns it, and the retry the state checkpointer runs goes through a freshdisableService()as any other restart does. Nothing in this branch had to move for it. The two commits are still 506 insertions and 67 deletions over the same five files; 325 is still claimed by nothing onmaster, and the file has no ordinal twice.Review round 1
catchpinned by no test.aSessionRestartWhichCouldNotRunIsRunAgainnow fails two restarts rather than one - the replay thread's and the checkpointer's - captures the error log over the delivery witherrorLogRecordsOf(), and asserts thatERR_REPLAY_SESSION_RESTART_FAILEDis reported exactly once before the change is delivered. Thecatch → rethrowmutant is red, and is the third row of the table above. The count of one assumes the replay thread runs its own request, which it does unless the checkpointer's tick lands in the instants between the request being made and being taken; the assertion says so.ieRunning()gate. Deliberate, and now said where it can be read: the javadoc ofrunPendingSessionRestart()and the bullet above. Both directions are in it - the import becauseownsItsSession()does not see an online total update into this replica at all, the export becauseexportLDIFEntry()gives the export up on the broker stop. The two synchronous roads during an export are Replication: a session restart run by the thread which released a change cuts the export the session carries #1048; during an import they are A failed entryUUID search reads as a deleted entry, and conflict resolution records the change as replayed #956 ([#956] Tell a failed entryUUID search apart from an entry which is not there #968).replay().enable(). Done, under the lock, first thing inenable(), with the window it closes in the comment.SessionRestartBackoffTest(new, two cases, a replication server and a domain of its own ono=testafter the pattern ofSessionRestartTest) pins both wakes, and each wake is pinned by its own case: the mutant table shows which case catches which.giveBack()takes what the caller asks for again, and its javadoc says what that is and why it is not whattake()returned; the class javadoc says which thread is owed the wait and which is not.aRestartGivenBackAfterItThrewIsOwedTheBackoffpins the merge for the production constant.The round is one commit on top of the two, c2455f8: 381 insertions and 15 deletions over five files,
SessionRestartBackoffTestthe new one. The three commits againstmasterare 874 insertions and 68 deletions over six files.Rebased onto
masterat eef0757, where #968 (#956) and #977 (#952) landedGit reported nothing, and the merge did not compile.
git merge-treeof the branch andmasterhad no conflicting hunk, GitHub said mergeable, and every CI leg of the previous head failed inBuild with Mavenon the merge commit it builds: #968 resets the restart request at the end ofimportBackend(), next to the pending changes and the backoff -sessionRestartRequested.set(false)- and the first commit of this branch replaces that field with the holder. Different parts of the file, so git merged both sides as they were. Fixed in that first commit, where the field goes:sessionRestarts.clear(), which is whatdisable()andenable()do on the same road. Arange-diffagainst the previous version of the three commits shows that line, the context #968 and #977 moved, and one comment: the checkpointer'srunPendingSessionRestart()was "run outside the block above", and #977 moved the save it referred to out of that block too, so it now says "outside the monitor above, as the save is".#977 met this change at the checkpointer, and the two agree. It takes
state.save()out of the checkpointer's monitor intosaveState(), which keeps a failed write to itself, and replaces thewhile (!done)ofshutdown()with a boundedjoin; this branch runsrunPendingSessionRestart()right after that save, outside the same monitor, and guards its own report.saveState()catchesRuntimeException, so anErrorout of the report would still end the thread there - the guard on the report stays. What #977 changes about the mutant table is thecatch → rethrowrow, re-run on this head: the checkpointer which ends on the rethrow no longer hangs the test JVM's cleanup, since #977'sjoin()returns at once for a thread which has ended - the case is red as before, and the JVM ends by itself, where before it spun ondone. What that mutant costs is the checkpointing of the domain, and the restart nobody else runs; not its shutdown.#968 met it at the roads which ask for the restart.
sessionHasAnOwner()-ownsItsSession() || importInProgress()- is whatrecoverFromReplayFailure(),abandonReplay()andrestartSession()now refuse the restart on, and the request this branch makes sits after that refusal on each of them, as the flag did. The checkpointer's gate isieRunning(), which coversimportInProgress()and the export besides, so it did not have to move. The import-end reset above is the one place the two touched the same state.Ordinals: #968 took 322, #977 took 323 and 324, this branch keeps 325; the file has no ordinal twice.
Review round 2
disable(). As measured. The sleep the table's row was run with was the one outside the monitor - the whole synchronized loop replaced by a sleep - which the row gave away without saying so: "the checkpoint did not come within 2500 ms" is a checkpointer which sleeps throughenable(), where one which sleeps inside the monitor holdsdisable()instead and then saves inside the bound. The case timesdisable()now, the way the shutdown case timesshutdown(), against the one bound both wakes share -WAKE_BOUND_IN_MS- and both shapes are rows of the table: inside the monitor, red ondisable()(2804 ms) and onshutdown()(2764 ms); outside it, red on the checkpoint and onshutdown()(2786 ms).reported == 1 || reported == 2, and the comment says which road gives two - the checkpointer's tick between the replay thread'srequest()and the CAS spends both failures - and why none has no road.restartSession()takes it in its first block, underserviceStateLock, before it lets go of the session it stopped. Read at the entry of the wait, the same window would sit between that block and the wait - adisable()and anenable()which both land there leave the count bumped already when it is read.disable()takesserviceStateLockbefore it wakes, so a wake not yet given when the block lets go is one the wait sees, whether it is under way by then or not, and one given before it is adisable()the block saw as the owner of the session. The session generation would have said the same and is not read: [#926] Restart the session of a replication domain in one place, under the lock and the generation #974 keeps it under the lock, and the wait is deliberately outside it.shutdown()wakes before it takes the lock and needs no count, its flag never comes back. Unpinned, and the table says so: the window is the checkpointer being descheduled for the length ofenable(), which no test can hit on purpose.requestSessionRestart()- the third@VisibleForTestingin the class, which the "say plainly" paragraph now lists. Theenable()case is the third ofSessionRestartBackoffTest; theimportBackend()case sits in [#956] Tell a failed entryUUID search apart from an entry which is not there #968'sReplayDuringImportTest, which has the import fixture: a request made while the import streams, a failure left to inject, and at the end of the import the failure is still there to spend and the session is up. And a correction to the rebase note: [#956] Tell a failed entryUUID search apart from an entry which is not there #968's first case runs through the import-end reset, it does not pin it - during an importabandonReplay()refuses onsessionHasAnOwner(), so nothing stands there to be cleared, and deleting the line survives that class. It does not survive the new case.aRestartAskedForWithoutTheBackoffIsGivenBackWithItparks a replayed delete with [#909] Cover the change a stopped replay thread hands back to the replication server #941's fixture, changes the number of replay threads so that the thread which holds it hands it back on its way out and asks for the restart without the backoff, and fails the restart the checkpointer runs for it. 1.5 s after the throw the session is still down: given back with the backoff, the request is taken on the checkpointer's next tick and waited a second, so the session is back two seconds after the throw; given back as taken, it is back on that tick. ThegiveBack(restart)mutant is red on it and green on every other case, as the round said it would be. It is also the first half of a pin the description said this PR had none of: the collapsed restarts of a thread-count change.startDomain()and thefinally. Assigned before the assert, as inSessionRestartTest, and the cleanup is onerelease()whose three steps are chained in nestedfinallyblocks.replay()re-runs what is standing on the same thread, backoff and all; only one which throws there as well is left to the checkpointer. The comment at the call says that now, the bullet above has its clause, and 317 says a restart is asked for rather than under way.The round is one commit on top of the three, c659a42: 374 insertions and 39 deletions over five files,
ReplayDuringImportTestthe one this PR had not touched before. The four commits againstmasterare 1214 insertions and 73 deletions over seven files.Rebased onto
masterat 600df92 in the same push, where #950 (#978), #913 (#979), #1046 (#1047) and #1031 (#1033) landed. Nothing met this branch: #978 is the one which touchesLDAPReplicationDomain, in the constructor and inpublishReplicaOfflineMsg(), andrange-diffshows all four commits moved as they were. 325 is still claimed by nothing onmaster- the four landings added tocore.properties, not toreplication.properties- and the file has no ordinal twice.