[#1055] Report a ReplicaOfflineMsg forwarded once it is written to the peer, not once it is queued - #1057
Conversation
a863e3a to
c577881
Compare
|
@maximthomas rebased onto master at The conflict was in Re-run after the rebase: Not measured again: the three mutations of the table. The sources under test are those the table was measured on. |
c577881 to
f287cbd
Compare
|
@maximthomas rebased onto master at
What #1019 changes for this PR. The peer speaking version 7 no longer reaches the session's refusal: the writer's filter drops its message first, logs it and gives the peer up. Measured again on this head:
The refusal left to the On this head: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The forward is now reported by the one thread that knows the write returned.
Session.run()callswritten(outgoing.whenWritten)only aftersend()returns, andcontinues past it onIOException(Session.java:619-621).publish(ReplicationMsg)keeps itsvoidsignature and delegates with a null callback (Session.java:327), so no caller without a callback changes.written()keeps a failing callback from taking the session thread down (Session.java:389-397).
f287cbd to
dacc944
Compare
|
@maximthomas thanks for the approval. #1035 landed right after it and the branch went conflicting, so it is rebased onto master at The conflict. #1035 made
What followed from it.
The On this head: |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The forward is reported from the thread that knows the write returned, and the rebase over #1035 gives both writers one contract.
Session.run()hitscontinueafter a failedsend()(Session.java:854), sowritten()(:856) never runs for a write that threw.- #1035's drain calls
written(outgoing.whenWritten)only after its ownsend()has returned (Session.java:370,:385). A ReplicaOfflineMsg that the close sends out of the queue is still reported, and one the close gives up on is not. ServerWriter.forwardReplicaOfflineMsg()callsreplicaOfflineMsgNotForwardedwhen the session refuses the message (ServerWriter.java:195), so the shutdown does not wait for a write that will never happen.
suggestion (non-blocking): No case pins the take-back refusal of publish(msg, whenWritten), so the mutant return true; at Session.java:560 survives.
opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/Session.java:560, :535-538, opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerWriter.java:193-196
A message whose offer succeeds after close() has cleared isRunning and drained the queue is taken back and reported as refused, as the javadoc and the description both say. No test reaches this arm. SessionPublisherDrainTest publishes after the close (:608, :650), but through publish(msg) after close() has returned, which takes the synchronous branch. Nothing in the replication protocol tests reaches takeBackWhatWasQueuedTooLate(). The only consumer of this false is the !accepted arm of forwardReplicaOfflineMsg(), which "Left out" already lists as unpinned. This road into that arm came with the rebase over #1035 and has its own mutant. Not run.
Pin: only a publish() that is already inside offer() when close() sets closeInitiated can reach the arm, and only if its offer lands after the drain's last poll. SessionTest's fixture cannot schedule that. A package-private seam in Session, between the successful offer and the isRunning read, would let a case run close() to completion, release the offer, and then assert that publish() returns false and the callback never runs. That kills return true; at :560.
nitpick (non-blocking): SessionTest's class javadoc says every case holds the session thread inside a write, but only the first case does.
opendj-server-legacy/src/test/java/org/opends/server/replication/protocol/SessionTest.java:44-47
newBlockingMsg() is published only at :76. In the second case the message is refused before any write, and the third case never starts the session thread. The 8 KiB buffers are set for every case, so the sentence describes the fixture, not each test.
* The peer of each test reads nothing until the test lets it, and both ends of the connection
* have socket buffers far smaller than {@link #BLOCKING_MESSAGE_SIZE}, so that a test which
* publishes a message of that size holds the session thread of the end under test inside its
* write for as long as it wants - the state in which a message published behind it is queued
* and not written.note (non-blocking): CI: the red cell is not this PR's.
build-maven (ubuntu-latest, 17)fails only onPDBStorageTest.testCanAddLargeValueswithOutOfMemoryError: Java heap space. That is the #1071 flake, which still shows up after #1072. In the same cellSessionTestpasses 3/3,SessionPublisherDrainTest9/9 andReplicationServerShutdownSyncTest21/21. Re-running the cell clears it.
… it is written to the peer, not once it is queued ServerWriter reported the forward as soon as Session.publish() had handed the message to the send queue of the session. A session thread busy writing an earlier buffer when the message was queued let the shutdown, released by that report, end its wait for a peer which had not been told and close the session: Session.close() sends what is still queued only once the write it joins has returned, and only within a budget of its own, not within the grace period the shutdown is meant to spend on that message. Session.publish(msg, whenWritten) runs the callback once, on the thread which wrote the message - the thread of the session, or the close for a message it sends out of the queue - after the write returned, and never for a message which was not written; it returns false for a message it neither wrote nor queued, and for one a close took back after draining the queue. ServerWriter reports the forward from that callback, and gives the peer up when the session refuses the message.
…er the close drained the queue A publish() which read the close as not yet begun and was descheduled before its offer can queue a message after the drain; publish() then takes it back, reports it and answers false, which is what the writer of a replication server reads as a refusal. No case reached that arm, so answering true there survived. Session gets a hook, run between the check of the close and the offer and set only by the tests, which holds the publishing thread at that spot while the close runs to its end. SessionTest's class javadoc no longer says every case holds the session thread inside a write: only the first one publishes a message of that size.
dacc944 to
7c7f055
Compare
|
@maximthomas thanks. All three points are taken. The branch is rebased onto master at suggestion - the take-back refusal of
So
The sending end keeps a reader across the close, as
This pins the session's side of the refusal, and the take-back #1035 left unpinned on master along with it. The writer's nitpick - SessionTest's class javadoc. Replaced with your wording: the buffers are what the fixture sets for every case, and holding the thread is what a test publishing a message of note - CI. Confirmed from the log of On this head: |
Fixes #1055
ServerWriterreported a ReplicaOfflineMsg forwarded as soon asSession.publish()had handedit to the send queue of the session. A session thread busy writing an earlier buffer when the
message was queued - a heartbeat, a TopologyMsg, an update to a peer whose TCP window is full -
let the shutdown, released by that report, end its wait for a peer which had not been told and
reach
close()with the message still queued. Before #1035close()dropped that queue: thepeer received the StopMsg and never the message the shutdown had been told was forwarded, and
its
ChangeNumberIndexerkept the medium consistency point pinned to the last CSN of thereplica until the replica came back - the cost of #917 and #983, on a peer the wait believed it
had served. Since #1035
close()sends what is still queued, but only once the write it joinshas returned and within a budget of its own (
DRAIN_BUDGET_MS), not within the grace period theshutdown is meant to spend on that message, and the shutdown still takes the peer for told
before it is. Recorded as a limitation by #919 and #947, whose wait proved the message was
handed to the session, not written.
The change
Session.publish(msg, whenWritten)runs the callback once, on the thread which wrote themessage, after the write returned - the thread of the session, or the close for a message it
sends out of the queue - and never for a message which was not written: one whose write
failed, or one the close gives up on (its budget ran out, its write failed, or the session had
already failed). It returns
falsefor a message it neither wrote nor queued: one the protocolversion of the peer cannot carry, one published while the session is being closed, or one
queued after the close drained the queue, which the close takes back.
publish(msg)delegatesto it, so nothing else changes for the messages which have no callback.
ServerWriterreports the forward from that callback, so "forwarded" now means written to thesocket - the bytes are with the kernel, which delivers them whatever the process does next,
since no replication socket sets
SO_LINGER- which is what the grace period is spent on. Amessage the session refuses is reported as a give-up, as the message the writer's own filter
drops already was ([#917] Wait for every peer replication server to forward the ReplicaOfflineMsg #947): nothing will ever forward it, and the shutdown must not wait for it.
Since [#1014] Report a ReplicaOfflineMsg as forwarded only to a peer which can decode it #1019 the filter drops a message the protocol version of the peer cannot carry before the
session is given it, so the refusal left to this road is the one of a session being closed.
Sessiongets a hook,beforeQueueing(Runnable), whichpublish()runs between its checkthat no close has begun and the offer of the message to the queue. Only the tests set it: a
publish()descheduled at that spot is the only one which can queue a message after a closehas drained the queue, and nothing else holds a thread there on cue. Unset, it costs one
volatile read.
The drain of
Session.close()is #1035's; this PR only carries the callback through it, so amessage the close writes on the way out is reported as forwarded rather than written in silence.
The shutdown does not reach
close()before the message is written unless the grace period runsout - and a session thread held inside a write is something
close()already waited for, in itsjoin().The tests
SessionTest(3, new) pins the contract of the callback on a session whose peer reads only whenthe test lets it: the callback does not run while the message is queued behind a write the peer
has not consumed, and runs once it has; a message with no encoding for the peer is refused and
its callback never runs; a session with no thread of its own writes on the publishing thread and
runs the callback before
publish()returns.SessionPublisherDrainTest(#1035's, 10, 2 new):aCloseRunsTheCallbackOfEachMessageItSendsOutOfTheQueuequeues seven messages with callbackson a session which was never started, closes it, and asserts that each callback ran once, in
the order of the writes, on the thread which closed the session, and that the peer received
exactly those messages.
aMessageQueuedAfterTheCloseDrainedTheQueueIsTakenBackAndReportedholds apublish()on astarted session at the hook above, runs
close()to its end, then lets the offer go. Itasserts that
publish()returnsfalse, that the callback never runs, that the queue isempty, that the take-back is reported once, and that the peer reads the StopMsg with nothing
before it.
aCloseWhichCannotSendTheQueueReportsEveryMessageTheQueueHeldgives its messages callbacks andasserts that none runs for a queue whose write failed.
Session$Outgoingelementpublish()queues, rather than a bare buffer.ReplicationServerShutdownSyncTest(21, 2 new):thePeerStillReadingAnEarlierChangeIsToldTheReplicaWentOfflineBeforeItIsStoppedholds thesession thread serving a peer inside the write of a 4 MiB change - the peer reads nothing and
the socket buffers on both sides of its connection are bounded to 8 KiB - queues the
ReplicaOfflineMsg behind it, starts the shutdown, and lets the peer read only once the
shutdown has either closed the session or spent a second not doing so. It asserts that the
peer receives the message, that no forward was reported before the peer read, that one was
reported by the time the shutdown returned, and that the shutdown returned inside the grace
period. Before Send what a replication session's publisher left queued when it is closed #1035 the peer received the StopMsg alone, with the forward reported by the
writer and the shutdown over in 39 ms; since Send what a replication session's publisher left queued when it is closed #1035 the drain of the close delivers the message,
and what fails is the forward reported while it was still queued.
theShutdownStopsWaitingForAPeerWhoseProtocolCannotCarryTheMessageconnects a peer speakingprotocol version 7, for which the message has no encoding, and asserts that the peer is struck
off and the shutdown ends inside the grace period. It was written against the refusal of the
session; since [#1014] Report a ReplicaOfflineMsg as forwarded only to a peer which can decode it #1019 the writer's filter drops that message first, so the case now runs the
road of [#1014] Report a ReplicaOfflineMsg as forwarded only to a peer which can decode it #1019 - which
onlyAPeerWhichCanDecodeTheMessageIsToldTheReplicaWentOfflinepins aswell - and it passes with the forward reported once
publish()returns (the first mutantbelow). It stays for the one assertion [#1014] Report a ReplicaOfflineMsg as forwarded only to a peer which can decode it #1019's case does not make: the duration of the
shutdown.
The peer of the first test is served over a connection the test established itself, as
ReplicationServer.runListen()serves an accepted one, because holding a thread inside a writeneeds the send buffer of the replication server's own socket bounded: the buffers a kernel picks
on its own on a loopback link absorb hundreds of kilobytes (and macOS keeps a few hundred on the
receiving side whatever it is asked for), so only an explicit
SO_SNDBUFon the accepted socketmakes the block deterministic.
Rebased onto master at
0793e27bbc, commits6ae52a56b6(the change) and7c7f055890(thetake-back case and its hook, from the review of
dacc944545). #1035 landed during the reviewand made
Session.close()send what the publisher left queued; the two met inSession:Outgoing(the buffer and its callback) and the drain of the close writesoutgoing.bufferand runs the callback after each write which returned;takeBackWhatWasQueuedTooLate()takes theOutgoingback and says whether it did, andpublish()returnsfalsefor a message taken back;close()drops the queue - the javadoc offorwardReplicaOfflineMsg()and two inReplicationServerShutdownSyncTest- say what it doesnow, and Send what a replication session's publisher left queued when it is closed #1035's
SessionPublisherDrainTestfills the queue withOutgoing(it failed with aClassCastExceptionotherwise).On this head:
SessionPublisherDrainTest10/10,SessionTest3/3,Skipped: 0. Ondacc944545, not repeated here (the commits master took since touch none of these files):ReplicationServerShutdownSyncTest21/21,DSRSShutdownSyncTest29/29,ReplicaOfflineMsgCatchUpTest2/2,SynchronizationMsgTest78/78.Every assertion was watched failing against the code it pins. The take-back row was measured
on this head; the first, third and last on
dacc944545; the second on an earlier head of thisPR and not again (
Session.run()is unchanged by the rebases):publish()returns, as on masterthePeerStillReadingAnEarlierChange...alone - the writer reported the message forwarded while it was still queued behind a change the peer had not readSessionTest.theCallbackRunsOnceTheMessageIsWrittenAndNotWhenItIsQueued- the callback did not run once the message had been written;thePeerStillReadingAnEarlierChange...- the message was written to the peer and nothing reported the forward; and the four existing cases which need a forward to be reported at allaCloseRunsTheCallbackOfEachMessageItSendsOutOfTheQueue- the close sent the queue without running the callback of each message it wrotepublish()answerstruefor a message queued after the drain (return true;in place of the take-back)aMessageQueuedAfterTheCloseDrainedTheQueueIsTakenBackAndReportedalone - a message queued after the close drained the queue was answered as queued, though nothing is left to send itRegression, on
f287cbdb1c:mvn -Pprecommit -pl opendj-server-legacy verifyoverSessionTest,ReplicationServerShutdownSyncTest,DSRSShutdownSyncTest,ReplicationServerDynamicConfTest,ReplicationServerTest,ReplicationDomainTest,MonitorTest,GenerationIdTest,TopologyViewTest,ReplicationServerFailoverTest,StateMachineTest,ChangelogBackendTestCase,FileChangelogDBTest,ChangeNumberControlPluginTestCase,ReSyncTest,SchemaReplicationTest,AssuredReplicationServerTest,AssuredReplicationPluginTest- 512 tests, no failures - andHandshakeAbortRegistrationTest,HandshakeAbortGenerationIdTeston their own (they collide on the administration connector port in a long run) - 5, no failures. Not repeated on this head; CI is the gate for it.Left out
!acceptedarm offorwardReplicaOfflineMsg()is not pinned any more. It was pinned bythe peer speaking version 7, whose message [#1014] Report a ReplicaOfflineMsg as forwarded only to a peer which can decode it #1019 now drops before the session is given it;
the refusal left to it is a message published while the session is being closed. The
session's side of that refusal - the take-back, and the
falseit answers - is pinned byaMessageQueuedAfterTheCloseDrainedTheQueueIsTakenBackAndReported. No case drives it throughthe writer, and the mutant which drops the writer's arm survives.
in the writer's filter, logs the drop and gives the peer up, on master.
Session.close()still blocks in itsjoin()for as long as the peer takes to read the bufferthe session thread is writing, and then in the writes of its drain and of the StopMsg, with no
bound on a single write but TCP's own: a peer which stops reading altogether holds the shutdown
of the replication server until the kernel gives the connection up. Older than this change,
recorded by Send what a replication session's publisher left queued when it is closed #1035 as well.