[#1014] Report a ReplicaOfflineMsg as forwarded only to a peer which can decode it - #1019
Conversation
ca2cf16 to
ca39ef5
Compare
|
Rebased onto master now that #947 has landed, and the description above is updated to what is left of the cost once the forward is waited for per peer. Nothing new is proposed here; this only says what the conflict was and how it was taken. The two guards are unchanged. What #947 changed is what they are worth: for an announcement The conflict was in three places:
Re-run after the rebase: |
|
Ordinal moved: 310 is claimed by #935 as well, under a different name ( The block moved to the end of the file rather than being renumbered where it stood: it sat between 309 and 319, and 328 there would have broken the ascending tail. That is where #968 and #977 ended up after their own moves, so the three agree - a renumbered message follows Nothing else moves with it. Not measured again: the tests pin the filter and the wait, neither of which reads the ordinal. |
5f7e04d to
f9f77fe
Compare
|
@maximthomas rebased onto master once more, now that #935 has landed (13d57e0, with #959, #975 and #976 in between), and the description is updated to the numbers below. Nothing new is proposed here; this only says what the conflict was and how it was taken. The conflict was in one place: 328 is still the first ordinal no open branch claims: 326 and 327 landed with #959, 321 to 325 are held by #964, #968, #977 and #981, and no open branch names 328 - checked on every open PR's copy of the file, not on the list from this morning. Re-run after the rebase: Not measured again: the two mutations of the table. The sources under test are those the table was measured on, and the ordinal is the only thing that moved, which no test reads. |
f9f77fe to
99cca92
Compare
|
@maximthomas rebased onto master once more, now that #958, #964, #974, #968 and #977 have landed (eef0757, 99cca92 now), and the description is updated to the run below. Nothing new is proposed here; this only says what the conflict was and how it was taken. The conflict was in one place: the tail of None of the five touches what this changes. #964 is the closest, in Re-run after the rebase: Not measured again: the two mutations of the table. The sources under test are those the table was measured on. |
99cca92 to
c0f796d
Compare
|
@maximthomas rebased onto master once more, now that #978, #980, #981, #984 and #987 have landed (129fc4e, c0f796d now), and the description is updated to the run below. Nothing new is proposed here; this only says what the conflict was and how it was taken. The conflict was in one place:
Re-run after the rebase: Not measured again: the two mutations of the table. The sources under test are those the table was measured on. |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The guard sits where every delivery road converges, and it reports what happened instead of pretending.
ServerWriter.isUpdateMsgFiltered()(ServerWriter.java:175-190) covers both the queue filled byput()and theReplicaCursorroad, and the drop lands asreplicaOfflineMsgNotForwarded()— a give-up #947's per-peer record can strike off, not a forward.ReplicationMsg.isEncodableFor()is the queryable twin of thenullingetBytes(), andSynchronizationMsgTest.replicaOfflineMsgHasNoEncodingBelowV8pins that the two cannot drift.- The V7 row of
onlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOfflineruns the real handshake throughFakePeerReplicationServer.connected(…, REPLICATION_PROTOCOL_V7)next to the V8 row as its positive control, and the DeleteMsg published after the announcement makes the per-peer assertions race-free.
issue (non-blocking): The version guard also warns for a pre-V8 directory server, and once per late-queue refill rather than once per message.
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerWriter.java:175-190, opendj-server-legacy/src/main/java/org/opends/server/replication/server/MessageHandler.java:533-551, :766-772
isEncodableFor() is checked ahead of the isDataServer() branch, so a directory server which negotiated V2..V7 and is catching up from the changelog reaches it: fillLateQueue() reopens getCursorFrom(serverState) on every refill, ReplicaCursor synthesizes the ReplicaOfflineMsg from the offline replica's CSN each time, and updateServerState() never records an offline CSN (by design), so the same CSN is re-served on every refill while the handler is not following. At BASE that copy died silently in Session.publish(); at HEAD each copy logs WARN 328 — "not sending update … to server " — for a peer which has no consumer for the message and for which put() already drops it without a word. Behaviour is unchanged (the DS road reports nothing to DSRSShutdownSync), so this is the log contract only; the description's "once per message" holds on the queue road, not on the cursor road it names as the reason the guard lives in the writer.
if (!updateMsg.isEncodableFor(handler.getProtocolVersion()))
{
if (!handler.isDataServer())
{
logger.warn(WARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEER,
handler.getReplicationServerId(), updateMsg.getCSN(), handler.getBaseDN(),
handler.getServerId(), session.getReadableRemoteAddress(),
handler.getProtocolVersion());
}
return true;
}Or: keep the WARN for every peer and trace it for a data server; either way, "once per copy the writer sees" in the description.
suggestion (non-blocking): WARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEER and its six arguments are pinned by no test.
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerWriter.java:186-189, opendj-server-legacy/src/test/java/org/opends/server/replication/server/ReplicationServerShutdownSyncTest.java:321-343
The V7 row asserts forwardedBy(), gaveUpOn(), dispatchedTo() and the peer's received messages; nothing reads the error log (git grep at the head: one reference under src/main, none under src/test). Deleting the logger.warn, permuting its arguments, or moving it so it fires per refill leaves ReplicationServerShutdownSyncTest 18/18 green — survival by construction, so no mutant was run. The call is right today (the six placeholders match the six arguments) and the neighbouring WARN_IGNORING_UPDATE_TO_* logs are equally unpinned, so this matches the class; it is the operator-visible half of the fix which has no pin.
// onlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOffline, around the publish and the drain
final AtomicReference<List<ReplicationMsg>> drained = new AtomicReference<>();
final List<String> records = errorLogRecordsOf(() -> {
shutdownSync.replicaOfflineMsgSent(baseDN, offlineCSN);
broker.publish(new ReplicaOfflineMsg(offlineCSN));
broker.publish(new DeleteMsg(DN.valueOf("uid=marker," + TEST_ROOT_DN_STRING),
csns.newCSN(), "22222222-2222-2222-2222-222222222222"));
drained.set(received.get(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS));
return null;
});
final List<ReplicationMsg> msgs = drained.get();
// ... the existing assertions on msgs and shutdownSync ...
if (!expectedToBeTold)
{
assertThat(records)
.filteredOn(r -> r.contains("severity=WARNING")
&& r.contains("msgID=" + WARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEER.ordinal())
&& r.contains("to server " + REMOTE_RS_ID))
.as("the drop for the peer speaking protocol version %s is logged once", peerVersion)
.hasSize(1);
}Pin: ReplicationTestCase.errorLogRecordsOf(Callable<Void>) (ReplicationTestCase.java:963) is already in the superclass; hasSize(1) pins "once per message", and the severity=WARNING term is what a bare msgID+text filter would leave green under a warn→info edit.
suggestion (non-blocking): put() still records a pre-V8 replication server among the recipients the shutdown waits for.
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServerDomain.java:411-417, :2390-2400, opendj-server-legacy/src/main/java/org/opends/server/replication/service/DSRSShutdownSync.java:495-499
Recorded only — the description lists "No filter in put()" under Left out. Its stated reason, "so that the give-up has something to strike off", is not load-bearing: giveUpOn(id) on an id not in awaitedForwarders is awaited.remove(id) && … == false, a no-op, and the writer guard covers the ReplicaCursor road on its own. What the record costs: with a V8 peer which has forwarded and a V7 peer catching up, the collocated DS shutdown waits on the V7 backlog up to REPLICA_OFFLINE_GRACE_PERIOD before that writer reaches the message and gives up — the same length as the phantom forward at BASE, so not a regression. hasPeerWhichCanReceiveReplicaOfflineMsgs() already applies the term put() would need. Neither new case covers a recorded announcement with a behind V7 peer; both use an idle one. The send-window argument in Left out stands either way.
// pushReplicaOfflineMsgToReplicationServers()
if (!isDifferentGenerationId(rsHandler, offlineMsg)
&& offlineMsg.isEncodableFor(rsHandler.getProtocolVersion()))
{
recipients.add(rsHandler);
recipientIds.add(rsHandler.getServerId());
}Or: leave it and say in Left out that the record is a wait on the peer's backlog, not a functional need.
nitpick (non-blocking): The test javadoc states the pre-#947 cost of a phantom forward.
opendj-server-legacy/src/test/java/org/opends/server/replication/server/ReplicationServerShutdownSyncTest.java:262-264
"… or the shutdown stops waiting for the peers which do need the message" — on the #947 base a forward reported by a recorded peer strikes off that peer alone (PendingOfflineMsg.forwardedBy(id), DSRSShutdownSync.java:474-487); the whole domain's wait ends on the first forward only for an announcement no recipient was recorded for, the distinction the description and the ServerWriter comment (:178-181) both make. The row's assertions are the per-peer model and are right.
* protocol it speaks - but the writer must not report that drop as a forward, or the shutdown
* counts this peer as told by a writer which sent nothing - and, for an announcement no
* recipient was recorded for, stops waiting for the peers which do need the message.nitpick (non-blocking): Replication server id 8234 is used by two cases of the class.
opendj-server-legacy/src/test/java/org/opends/server/replication/server/ReplicationServerShutdownSyncTest.java:1003, :370
shutdownIsNotDelayedWhenNoConnectedPeerCanDecodeTheMessage builds newReplicationServer(shutdownSync, "shutdownSyncOldPeerDb", 8234); thePeerWhoseHandshakeIsInFlight… already has 8234, every other case its own id (8221-8230). Cosmetic — the class runs sequentially and each case removes its server in finally — until a leaked server or a monitor entry named RS(8234) is attributed to the wrong case.
newReplicationServer(shutdownSync, "shutdownSyncOldPeerDb", 8231);note (non-blocking): Not checked — nothing open.
- Execution evidence for the 18 cases and
SynchronizationMsgTestat this head is CI run 35103446267 (ubuntu 11/17/21/25/26 green); no local run and no mutant was spent this round — the V7 row'sgaveUpOn() == [92]cannot pass with the writer reading 8, which settles the V7 negotiation. - Every message the RS emits to the V7 fake peer decodes at V7:
ReplicaOfflineMsgis the only type gated at V8 (ReplicationMsg.java:85), and the guard withholds it. - The DS-side twin (
broker.publish()to a V7 RS, #949) is outside this PR, as Left out says.
|
@maximthomas thanks - four taken, one recorded with the reason in the description corrected, and The version guard also warns for a pre-V8 directory server, and once per late-queue refill. One correction to the reading, which does not change the outcome: the repetition is not specific to
Pinning the count needed one thing your snippet did not have: the case now waits for the peer to be Four mutations, each run on the parametrized case alone:
The filter is still left out, for two reasons: the version term would live in a second place while The test javadoc states the pre-#947 cost. Taken, in your wording. Replication server id 8234 is used by two cases. Taken, with 8236 rather than the 8231 of the Run at this head: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The round-1 pin was taken as written and the delta says why each arm exists.
ReplicationServerShutdownSyncTest.onlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOffline:311-397runs publish-and-drain insideerrorLogRecordsOf()and pins the V7 record onhasSize(1)plusseverity=WARNING,RS(id), the CSN, the domain and the version — an argument dropped or permuted fails there, and the V8 row'sisEmpty()fails a guard which reports every message.waitForFollowing():1313-1331pollsMessageHandler.isFollowing()before the announcement, so how many copies the writer takes — and reports — is no longer a race with the changelog cursor.ServerWriter.isUpdateMsgFiltered():186-203traces the directory-server drop and the comment names the mechanism (cursor road, offline CSN never in the handler state, once per refill).
issue (non-blocking): Every update the writer filters has already cost a send-window permit the peer never acks — pre-existing, on every filter arm.
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerHandler.java:991-994, :1059-1071, :1105, opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerWriter.java:175, :215-260
take() is getNextMessage() → acquirePermitInSendWindow() with no filter between; the only release is updateWindow()'s sendWindow.release(windowMsg.getNumAck()), and the peer acks only what it received (decAndCheckWindow:327-329, ReplicationBroker:2512). A message isUpdateMsgFiltered() drops — this arm or BAD_GEN_ID / FULL_UPDATE / WARN_IGNORING_UPDATE_TO_RS — never reaches the peer, so its permit is gone for the session; after windowSize (default 100) such drops the writer spins in tryAcquire(500 ms) and the peer receives nothing until reconnect. Not this PR's doing: at the base Session.publish() dropped the same message one step later (Session.java:308-314) after the same permit — the drop moved, the count did not. A follow-up issue; not run, the stall is derived from the three sites.
// ServerHandler
void releasePermitInSendWindow()
{
sendWindow.release();
}
// ServerWriter.isUpdateMsgFiltered(), at every `return true`
handler.releasePermitInSendWindow();
return true;Or: take the permit after the filter passes.
suggestion (non-blocking): On the peer-RS road WARN 328 fires once per copy, and a pre-V8 replication server catching up is handed the same announcement once per late-queue refill.
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerWriter.java:204-211
Recorded only — the description states per-copy reporting as the choice. The reasoning the delta applies to the directory-server road holds for the cursor copies here too: ReplicationServerHandler reads through the same fillLateQueue() (MessageHandler.java:533-556, ≤100 msgs per refill, cursor reopened from serverState each time), ReplicaCursor.next():129-137 re-emits the message while the offline CSN is not covered, and only the queued copy — the one put() recorded a recipient for — strikes the peer off; every cursor copy's replicaOfflineMsgNotForwarded() is a no-op. A pre-OPENDJ-1453 peer N changes behind logs ~N/100 records for one announcement. The writer can tell the two copies apart: take() already reads isLastMessageFromLateQueue() (ServerHandler.java:992, protected in MessageHandler:277, same package).
if (handler.isDataServer() || handler.isLastMessageFromLateQueue())
{
// a cursor copy: re-served once per refill, it reports nothing the queued copy did not
if (logger.isTraceEnabled()) { logger.trace(...); }
}
else
{
logger.warn(WARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEER, ...);
}One corner: a queued copy evicted by the msgQueue overflow (MessageHandler.java:154) is then served by the cursor and would be traced; a once-per-(peer, offline CSN) memo in the handler avoids that at the cost of a field.
suggestion (non-blocking): The directory-server arm of the drop report — trace, not WARN — is entered by no test.
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerWriter.java:186-203
Every pre-V8 fixture under src/test is a replication server: the FakePeerReplicationServer of the [V7,false] row (:263) and the raw session registered as a connected RS in shutdownIsNotDelayedWhenNoConnectedPeerCanDecodeTheMessage (:1073); the DS of the version rows is openReplicationSession(baseDN, LOCAL_DS_ID, …) at the current version. The mutant if (handler.isDataServer()) → if (false) — WARN 328 per refill for a pre-V8 DS, the round-1 shape this delta removes — leaves both rows green: the V7 row counts records naming to server 92 only. The inverse mutant is killed by hasSize(1); one side of the split is pinned. Deferred — the DS drop is a stated non-event and the fixture is the cost.
// a DS session hand-rolled at REPLICATION_PROTOCOL_V7 (the :1073 shape with a ServerStartMsg),
// connected with an empty state after an offline CSN is in the changelog, then:
final List<String> records = errorLogRecordsOf(() -> { /* publish the marker, drain */ return null; });
assertThat(records)
.as("a drop for a directory server is traced, not reported")
.noneMatch(r -> r.contains("msgID=" + WARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEER.ordinal())
&& r.contains("to server " + oldDsId));suggestion (non-blocking): hasPeerWhichCanReceiveReplicaOfflineMsgs() is pinned at the two extremes only; a mixed V7+V8 topology is exercised by no case.
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServerDomain.java:2390-2400
The only pre-V8 peers under src/test are single (ReplicationServerShutdownSyncTest:263, :1069-1078); every multi-peer case (:664-666, :918-920) connects current-version peers. The mutant which decides on the first handler iterated — loop body if (isSupportedBy(v)) return true; → return isSupportedBy(v); — is equivalent on one peer, on all-V8 peers and on none, so a regression which skips the wait for a V8 peer because a V7 peer came first stays green. Deferred.
Pin: the :1069-1078 shape with two registered sessions, one at V7 and one at V8, on an announcement no recipient was recorded for — awaitReplicaOfflineMsgsForwarded() must wait; the mutant returns false when the V7 handler is iterated first and ends the wait at once. Register order is not iteration order (ConcurrentHashMap), so assert the wait, not which peer decided it.
nitpick (non-blocking): waitForFollowing() was inserted between the javadoc of waitForConnectedReplicationServer() and that method.
opendj-server-legacy/src/test/java/org/opends/server/replication/server/ReplicationServerShutdownSyncTest.java:1300-1305, :1334
The 40-line handshake-window comment ("The registration is not the end of the handshake …") now ends at :1303 directly above waitForFollowing()'s own /**; javac attaches only the immediately preceding doc comment, so it dangles and waitForConnectedReplicationServer() is undocumented. Move the waitForFollowing() block above that comment or below waitForConnectedReplicationServer().
…nly to a peer which can decode it ReplicaOfflineMsg has no encoding below replication protocol V8: Session.publish() drops what it cannot encode for its peer and returns as if it had sent it, and ServerWriter went on to report the forward regardless of the negotiated version. The shutdown then recorded a peer as told which was sent nothing - and, for an announcement whose recipients were never recorded, believed it relayed and stopped waiting for the peers which still needed it. The phantom forward comes from the writer with no wire work to do, so it systematically won that race. ServerWriter now filters an update the peer cannot decode before it publishes, and logs it: there is genuinely nothing to send, since the message is not part of the protocol version that peer speaks. A ReplicaOfflineMsg dropped there turns into the give-up of OpenIdentityPlatform#947 for that peer, as it does for the generation id filters. The check goes through ReplicationMsg.isEncodableFor(), the queryable form of the null getBytes() already documents, so a message version-gated in the future is covered without a new instanceof. ReplicationServer.awaitReplicaOfflineMsgsForwarded() no longer counts such a peer as somebody to wait for, otherwise a topology whose only peer predates V8 would spend the whole grace period on a message nothing can forward.
…t decode where it is an event, and pin it The version guard warned for every peer it dropped a message for, a directory server included: ReplicationServerDomain.put() never queues a ReplicaOfflineMsg for one and says nothing when it drops it, so the copies which reach the writer there are the ones ReplicaCursor synthesizes while the handler is catching up - and, since an offline CSN never enters the state of a handler, again on every refill of its late queue. That road is traced now; the warning stays for a peer replication server, where the drop is what the shutdown accounts for. The message and its arguments were pinned by no test. Both rows of onlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOffline read the error log now: the V7 row expects one record naming this replication server, the domain, the change and the version it has no encoding for, published as a warning; the V8 row expects none. The case waits for the peer to be served from its queue first, so that the writer is handed the announcement once and the count is not a race with the changelog road. Also: the javadoc of the case stated the cost of a phantom forward as it was before OpenIdentityPlatform#947, and the case pinning the caller side check shared the server id 8234 with the handshake case of OpenIdentityPlatform#987.
…and pin a mixed topology The arm which traced the drop for a directory server has no way in left: OpenIdentityPlatform#1029 gave DataServerHandler its own updateServerState(), which drops a message that does not contribute to the domain state before getNextMessage() hands it to the writer, so a ReplicaOfflineMsg - the only message gated on a protocol version - never reaches the writer of a directory server on either road. The arm is removed rather than pinned: the guard reports the drop whoever the consumer is, and the comment says why a directory server does not get here. shutdownWaitsWhenOnlySomeOfTheConnectedPeersCanDecodeTheMessage registers two raw sessions on one domain, one at V7 and one at the current version, and asserts the shutdown waits out the grace period for an announcement no recipient was recorded for. It asserts the wait, not which peer decided it: the iteration order of connectedRSs is not the registration order. waitForConnectedReplicationServer() moved back under the javadoc it owns, which waitForFollowing() had been inserted into.
5bfd31b to
8b1c504
Compare
|
@maximthomas two taken, one settled by a change which landed on master after this review, one Every update the writer filters has already cost a send-window permit. Confirmed, and it is One correction to the size: Not this PR either way - the drop moved one step, the count did not - and the arms which can hit On the peer-RS road WARN 328 fires once per copy. Recorded, and it stays per copy. The two The directory-server arm of the drop report is entered by no test. It is entered by nothing So the split is gone rather than pinned: the guard reports the drop whoever the consumer is, and
Run at this head:
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: The delta takes the round-2 mixed-topology gift and removes an arm that could never be reached.
ServerWriter.isUpdateMsgFiltered():186-197has one report road now. The premise holds:DataServerHandler.updateServerState():322returns false for a message that does not contribute to the domain state, andMessageHandler.getNextMessage()hands a message out only when that call returns true (:374,:405,:439). So aReplicaOfflineMsgnever reaches a directory server's writer.ReplicationServerShutdownSyncTest.shutdownWaitsWhenOnlySomeOfTheConnectedPeersCanDecodeTheMessage:1113-1155puts a V7 peer and a current peer in one domain. It kills the first-handler-decides and all-must-decode mutants ofReplicationServerDomain.hasPeerWhichCanReceiveReplicaOfflineMsgs():2390. It passed locally at the head:tests="1", 5.4 s.
Fixes #1014
The bug
ReplicaOfflineMsghas no encoding below replication protocol V8(
ReplicaOfflineMsg.getBytes()returnsnullthere),Session.publish()treats thatnullas "nothing to send" and returns as if it had sent it, and
ServerWriterreports the forwardregardless of the version negotiated with the peer. Nothing between the two looks at that
version, so the shutdown bookkeeping records a delivery which never reached a socket.
The phantom forward is not a race the peer might lose. The writer of a pre-V8 peer has no
serialization and no socket write to do for that message, so it is the fastest possible path to
replicaOfflineMsgForwarded().What that costs changed with #947, which landed while this was open and which this is now
rebased onto. Before it, one forward ended the wait of the whole domain: with one pre-V8 peer in
the topology, the entry the shutdown watches was systematically cleared before the writer of a
V8 peer had drained its queue,
stopAllServers(true)then cleared that queue, and the peerwhich could have been told never was - the cost #917 describes, caused deterministically
rather than by scheduling. #947 records the recipients of the message when
put()queues itand waits for each of them, so for such a message the phantom forward now strikes off only the
peer which reported it - which is where a pre-V8 peer has to end up, but as a peer which was
told, by a writer which sent nothing. What is left of the cost:
awaitReplicaOfflineMsgsForwarded()leaves to the first forward: the message of a replicawhich picked a remote replication server, the late announcement of A ReplicaOfflineMsg still queued behind an uncommitted change is recorded as sent, and the shutdown then waits its whole grace period for nothing #918 - still ends the wait
of the whole domain on the first forward, whoever reports it. The copy
ReplicaCursorsynthesizes from the offline CSN for a peer which is catching up is how the writer of a pre-V8
peer gets to be that first forward.
ReplicationServer.awaitReplicaOfflineMsgsForwarded()still counts a pre-V8 peer as somebodyto wait for: in a topology whose only peer predates V8, such an announcement waits out the
whole grace period, since nothing can ever report a forward there.
A peer which negotiated V2..V7 can be there:
ProtocolVersion.getCompatibleVersion()clampsrather than refuses, there is no minimum supported version, and on the accept side
replicationServerDomain.register(this)sits outside the> REPLICATION_PROTOCOL_V1block, sosuch a peer is registered in
connectedRSsand served by a fullServerWriter.One correction to the issue
The issue attributes the cost to the old peer ("a peer left unaware keeps the replica in
ChangeNumberIndexer.getOldestLastAliveCSN()"). A peer which cannot decode the message predatesit - V8 exists because of
ReplicaOfflineMsg(OPENDJ-1453), andChangeNumberIndexerarrivedwith it - so it has no consumer for the announcement and loses nothing. Not telling it is a
property of the protocol, not a defect. What is a defect is calling that a forward: the peers
which do run a
ChangeNumberIndexerare the ones which pay for it.The fix
ServerWriter.isUpdateMsgFiltered()filters an update the peer cannot decode, before itis published, and reports it with
WARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEERonce per copy ittakes - there is genuinely nothing to send. Per copy, not per announcement: the changelog road
hands the message to the writer again on every refill of the late queue, since the offline CSN
never enters the state of the handler (
MessageHandler.updateServerState()leaves it out bydesign, and
ReplicaCursorsynthesizes the message anew from the offline CSN each time thecursor is reopened). The record does not distinguish consumers, because only one kind of
consumer can get here: since A ReplicaOfflineMsg sent to a directory server on the catch-up path takes a send-window permit the session never gets back #1029 a
ReplicaOfflineMsgnever reaches the writer of adirectory server at all -
put()does not queue it for one, andDataServerHandler.updateServerState()drops the copy the changelog cursor of a directoryserver which is catching up synthesizes - and
ReplicaOfflineMsgis the only message gated ona protocol version. This is the single point every delivery path passes through: the in-memory queue filled by
ReplicationServerDomain.put(), and the changelog cursor of a peer which is catching up,where
ReplicaCursorsynthesizes aReplicaOfflineMsgfrom the offline CSN and which afilter in
put()would not cover. On top of [#917] Wait for every peer replication server to forward the ReplicaOfflineMsg #947 a drop here is what it already is for thegeneration id filters: the writer reports
replicaOfflineMsgNotForwarded()for the peer, so apre-V8 peer the message was queued for is struck off by its own writer as a peer which will
not forward it, rather than credited with a forward.
ReplicationMsg.isEncodableFor()(defaulttrue, overridden byReplicaOfflineMsg), the queryable form of thenullreturngetBytes()already documents.A message version-gated in a future protocol version is then covered without a new
instanceof- the trap here is in the extension mechanism, not only in this one message.ReplicationServer.awaitReplicaOfflineMsgsForwarded()no longer counts such a peer assomebody to wait for. Its check was
!domain.getConnectedRSs().isEmpty(), a stand-in for"there is somebody to forward the message to"; without narrowing it, the fix would turn the
grace period ends too early into the grace period is always exhausted for an announcement no
recipient was recorded for, in a topology whose only peer predates V8. An announcement
put()did record recipients for no longer needs this on top of [#917] Wait for every peer replication server to forward the ReplicaOfflineMsg #947 - the give-up of the writer ends
its wait - which is what its javadoc now says.
The hot path keeps one virtual call returning a constant for every other message.
Tests
ReplicationServerShutdownSyncTest:onlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOfflinedrives a peer through the realhandshake at a chosen protocol version and asserts, for V8 and for V7, both what the peer
received and what the writer reported to
DSRSShutdownSync- through theRecordingShutdownSyncof [#917] Wait for every peer replication server to forward the ReplicaOfflineMsg #947, per peer: the V8 row expects the peer inforwardedBy(), theV7 row expects it absent from
forwardedBy()and present ingaveUpOn(), and both expect itin
dispatchedTo(), so that a writer which was never handed the message cannot pass eitherrow. The V8 row is the positive control, so the V7 row cannot pass vacuously. The change
published after the announcement is the synchronization point - the writer of a peer takes
from one queue in order, so a peer which received that change has already dealt with the
announcement which precedes it, and the assertions on what the writer reported cannot race.
Both rows also read the error log of the run, through the
errorLogRecordsOf()ofReplicationTestCase: the V7 row expects exactly one record ofWARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEERfor the peer, published as a warning and namingthis replication server, the domain, the change and the version it has no encoding for; the
V8 row expects none, so a guard which reported every message would fail there. The records
are picked by message id and peer rather than by their whole text - the address the writer
logs is the one the replication server sees the peer on, which the test side does not hold.
Both rows wait for the peer to be served from its queue before the announcement is published
(
MessageHandler.isFollowing(), the accessor A replica which reconnects after a restore can be declared up to date and never sent the change it was restored past (ReSyncTest.testResyncAfterRestore) #963 left for this): a peer which is stillcatching up reads the changelog too, where
ReplicaCursorsynthesizes a second copy of thesame announcement, and how many copies its writer takes - hence how many drops it reports -
would be a race.
shutdownIsNotDelayedWhenNoConnectedPeerCanDecodeTheMessagepins the caller side check, on anannouncement no recipient was recorded for.
shutdownWaitsWhenOnlySomeOfTheConnectedPeersCanDecodeTheMessagepins the other side of thatcheck, on a mixed topology: two raw sessions registered on one domain, one at V7 and one at the
current version, and an announcement no recipient was recorded for. The shutdown must wait out
the whole grace period, for the sake of the peer which can still be told. What it asserts is
the wait and not which peer decided it: the iteration order of
connectedRSsis not the orderthe two are registered in.
SynchronizationMsgTest.replicaOfflineMsgHasNoEncodingBelowV8pins the protocol floor itself,so that
getBytes()andisEncodableFor()cannot drift apart.Both new assertions were watched failing against the code they pin, again after the rebase
onto #947:
ServerWriteronlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOffline[7, false]- the writer of the peer speaking protocol version 7 reported a forward of a message which never reached it, expected empty but was[92]awaitReplicaOfflineMsgsForwarded()shutdownIsNotDelayedWhenNoConnectedPeerCanDecodeTheMessage- the shutdown waited for a peer which cannot decode the message, 5018 msUnder the first mutation the V8 row passed next to the failing V7 one, so the failure is the
missing guard and not the harness; each mutation was run on its own test only.
The assertions on the log added in this round were watched failing the same way, each mutation
on the parametrized case alone:
logger.warnof the drop removed[7, false]- the drop for the peer speaking protocol version 7 should be logged once, expected size 1 but was 0[8, true]a message the peer speaking protocol version 8 did receive was reported as dropped, expecting empty but holding the announcement and the change which follows it;[7, false]expected size 1 but was 3logger.warndemoted tologger.info[7, false]- the drop should be reported as a warning naming this replication server, the domain, the change and the version it has no encoding for, the record holdsseverity=NOTICE[7, false]- should be logged once, expected size 1 but was 0: no record names the peer as the server the update was not sent toRegression, before the rebase:
ReplicationServerTest,AssuredReplicationServerTest,ProtocolCompatibilityTest,ProtocolWindowTest,GenerationIdTest,StateMachineTest,ReplicationServerFailoverTest,ReplicationServerDynamicConfTest,TopologyViewTest,DSRSShutdownSyncTest,MonitorTest,ChangelogBackendTestCase,ChangeNumberControlPluginTestCase- 496 tests. The only failure wasMonitorTest.setUpbinding the administration connector to 0.0.0.0:65534, the port collision several server tests
in one JVM produce; the class passes on its own. After the rebase onto #947:
ReplicationServerShutdownSyncTest16/16 andSynchronizationMsgTest76/76. After the rebase onto#935 and #976, whose only conflict was the ordinal of
WARN_IGNORING_UPDATE_UNSUPPORTED_BY_PEER(328 now, at the end of
replication.properties; the sources are byte for byte those of the head thetable above was measured on):
ReplicationServerShutdownSyncTest16/16 andSynchronizationMsgTest78/78, the two rows added by #975 included. After the rebase onto #958, #964, #974, #968 and #977,
whose only conflict was again the tail of
replication.properties- master's 322 to 324 are keptahead of 328, nothing is renumbered, and the sources are still byte for byte the same:
ReplicationServerShutdownSyncTest16/16 andSynchronizationMsgTest78/78. After the rebase onto#978, #980, #981, #984 and #987, whose one conflict was
FakePeerReplicationServer- #987 split itshandshake into the two phases
handshaking()andcompleteHandshake()need and turned theconstructors into the factories
connected(...), so the protocol version rides on that shape now:connected(port, id, baseDN, generationId, windowSize, protocolVersion)is the factory the testcalls, the private constructor takes the version and announces it in the start message as before,
and
handshaking()passes the current one; the guard and the check are still byte for byte thesame:
ReplicationServerShutdownSyncTest18/18, the two cases of #987 included,SynchronizationMsgTest78/78 and
DSRSShutdownSyncTest29/29. After the first review round, whose changes are the traceon the directory server road, the assertions on the log, the wait for the peer to follow its queue
and one server id:
ReplicationServerShutdownSyncTest18/18. After the rebase onto the currentmaster - 28 commits, #1029 among them, and the one conflict was again the tail of
replication.properties, where master's 329 is kept and 328 is still free - and after the secondreview round, whose changes are the guard reporting the drop whoever the consumer is, the case on
a mixed topology and the javadoc of one helper:
ReplicationServerShutdownSyncTest19/19,SynchronizationMsgTest78/78 and theReplicaOfflineMsgCatchUpTestof #1029 2/2.The assertions added in the second round were watched failing as well, each mutation run on the
cases it is meant for:
hasPeerWhichCanReceiveReplicaOfflineMsgs()deciding on the first handler iterated (return isSupportedBy(...)in the loop body)shutdownWaitsWhenOnlySomeOfTheConnectedPeersCanDecodeTheMessage- the shutdown stopped waiting although a connected peer can still be told that the replica went offline, 2006 ms against the 5000 ms of the grace period;shutdownIsNotDelayedWhenNoConnectedPeerCanDecodeTheMessagestays green, so it is the mixed topology which catches itlogger.warnof the drop removed, now that the guard has one armonlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOffline[7, false]- the drop for the peer speaking protocol version 7 should be logged once, expected size 1 but was 0Left out
ReplicationServerDomain.put(). It would save a queue slot and a send windowpermit, but it does not cover the changelog cursor path, and the permit it would save is leaked
by every filtered message anyway - see below. The record
put()keeps for a pre-V8 peer isnot what makes the give-up of [#917] Wait for every peer replication server to forward the ReplicaOfflineMsg #947 work:
giveUpOn()on an id no recipient list holds is ano-op. What that record costs is a wait on the backlog of that peer - with a V8 peer which has
forwarded and a pre-V8 peer which is behind, the shutdown waits until the writer of the
pre-V8 peer reaches the message and gives up, up to the whole grace period. That is where the
phantom forward was reported before this change - the writer takes from one queue in order
either way - so nothing takes longer than it did. A filter in
put()would shorten that wait,at the price of the version term living in a second place while the writer still has to hold
it for the changelog road, and of the queue road - the road by which a test can hand the
message to a writer at all - no longer reaching the guard.
ServerHandler.take()acquiresit before any filter runs, the only release is
updateWindow()on what the peer acked, and apeer acks only what it received - so every filtered message costs the session a permit for the
rest of it, and after
windowSizesuch drops the writer sits intryAcquire(500 ms)and thepeer receives nothing until it reconnects. That is true of the generation id and status filters
which were there before, and the drop this PR adds only moves one such loss one step earlier
(at the base
Session.publish()dropped the same message after the same permit).windowSizeis what the peer advertised in its start message, 100000 by default, so it takes as many drops
to reach the stall. Raised as A message the writer filters keeps the send-window permit it took #1080 rather than changed here.
broker.publish()returningfalse,but a broker whose replication server predates V8 returns
truewhile the session drops themessage, so that hole stays open after A ReplicaOfflineMsg the broker refuses is still recorded as sent: ReplicationDomain.publish() discards the outcome #949 too.
MonitorMsg.getBytes()returnsnullwhen its encoding throws, andSession.publish()swallows that the same way. It is the only other message which can produce a
nullencoding,and it is a different defect.