[#950] Announce a ReplicaOfflineMsg before it is published, not after it may have been forwarded - #978
Conversation
8175d6e to
848c47c
Compare
|
Rebased onto master now that #946 has landed. The conflict both PRs predicted is resolved and the description above is updated to match; no review had been posted yet, so nothing here answers a review comment. The two changes met in the same three files.
final CSN offlineCSN = pendingChanges.putReplicaOfflineMsg();
if (offlineCSN == null && logger.isTraceEnabled())
{
/*
* The announcement itself is made where the message is published, so nothing has to be
* reported here: a message which never reached the wire was never announced either.
*/
logger.trace("Replica " + getServerId() + " of domain baseDN=" + getBaseDN()
+ " could not announce itself offline: a change which is still in flight holds"
+ " the message back, and " + pendingChanges.size() + " change(s) are pending");
}
One case needed more than a merge, and it is worth naming.
|
|
For the record, since the run on the pre-rebase head 8175d6e went red: The map this branch moves the write of - It looks like #924; the evidence, and a second sighting of the same signature on another |
848c47c to
ec70866
Compare
|
Rebased onto master ( Only This finishes the note above about the red run on the pre-rebase head. That failure now has a name: Verified on the rebased branch rather than on the old head:
|
…published, not after it may have been forwarded The announcement the shutdown of a collocated replication server waits on was recorded after PendingChanges.putReplicaOfflineMsg() had already put the message on the wire. A forward which won that race found nothing to clear, and the announcement which followed it was one nothing would ever remove: ReplicationServer.shutdown() then spent the whole REPLICA_OFFLINE_GRACE_PERIOD waiting for the forward of a message the topology already had. The announcement now sits where the message is published - the ReplicaOfflineMsg branch of pushCommittedChanges() - so it is in place before session.publish() is reached and the forward cannot precede it. Announcing at the publish site also makes it follow the publication rather than the queueing, which leaves the guard OpenIdentityPlatform#918 put around the announcement nothing to do: a message a change in flight holds back is not published, and is therefore not announced either. The trace which reports such a message stays. The broker may still refuse the message once it is announced - no usable session, a recovery pending, or stopped in between - which OpenIdentityPlatform#949 made domain.publish() report. Such an announcement is one nobody will ever forward, so it is withdrawn through the new DSRSShutdownSync.replicaOfflineMsgNotSent(), which removes only the entry carrying that CSN and wakes the shutdown up as a forward does: what is announced is what really went out. New PendingChangesTest cases: the forward reported from within publish() no longer leaves an announcement behind, the announcement of a message the broker refused is withdrawn, and a message held back by a change in flight is never announced - neither while it waits, nor when the change which held it back completes and the message is given up on. DSRSShutdownSyncTest covers the withdrawal: it ends the wait, it wakes a waiting shutdown up, and it leaves a newer announcement of the same replica alone.
ec70866 to
4aeb3b3
Compare
|
Rebased onto master ( The conflict was with #976, in the final CSN offlineCSN = msg.getCSN();
replicaOfflineAnnouncer.announce(offlineCSN);
if (domain.publish(msg))
{
publishedOfflineCSN = offlineCSN;
}
else
{
// The broker wrote it to no session, so nobody will forward what was announced.
replicaOfflineAnnouncer.withdraw(offlineCSN);
}
#947 changed Tests, on the rebased head:
Both regressions were watched: with the announcement moved back behind The description above is updated to match. |
Fixes #950
The bug
LDAPReplicationDomain.publishReplicaOfflineMsg()recorded the announcement afterpendingChanges.putReplicaOfflineMsg()returned, and that call has already put the message onthe wire:
pushCommittedChanges()reachesdomain.publish(msg)->ReplicationBroker.publish()->
session.publish(msg)before it comes back.A collocated replication server which forwards the message in that window calls
DSRSShutdownSync.replicaOfflineMsgForwarded()from itsServerWriter, which finds no entry forthe replica and does nothing but notify the monitor.
replicaOfflineMsgSent()then installs aPendingOfflineMsgwhich nothing will ever remove - the forward it was waiting for has alreadyhappened.
Since #919 that record is the condition of a blocking wait:
ReplicationServer.shutdown()callsawaitReplicaOfflineMsgsForwarded()and, with a peer RS connected, spends the wholeREPLICA_OFFLINE_GRACE_PERIODon a message which is on the wire and forwarded. Nothing is lost -the topology has the announcement - it is a bounded delay of the shutdown. Before #919 the stale
record was harmless, and the ordering it depends on has been there since OPENDJ-1453.
#946 has since narrowed which messages are announced - only those which really were published -
but left the ordering alone: the announcement of a published message still follows its publish.
The window is narrow: between the return of
session.publish()and the next statement of thepublishing thread, the collocated RS has to read the socket, write the changelog, queue the
message on the peer handler and write it to the peer session. But the cost of losing the race is
precisely the delay the grace period exists to bound.
The change
The announcement moves to the point where the message is published - the
ReplicaOfflineMsgbranch of
PendingChanges.pushCommittedChanges()- through aReplicaOfflineAnnouncerthedomain hands to its
PendingChanges. It is therefore in place beforesession.publish()isreached, and the
ConcurrentHashMapit is written to gives the forwarding thread, which reads itonly after reading the socket, the visibility it needs. The forward can no longer precede it.
Announcing at the publish site, rather than before the whole
putReplicaOfflineMsg(), also meansthe announcement follows the publication instead of the queueing. A message which a change in
flight holds back (#918) is not announced at all: #946 gives up on such a message rather than
letting it out late, so there is no later publish to announce it at.
That leaves the
if (offlineCSN != null)guard #946 put around the announcement nothing to do,which is what its own description predicted: a message which is not published is not announced.
publishReplicaOfflineMsg()keeps only the trace #946 added, with the wording #976 gave it.One announcement does have to be withdrawn. Since #976
domain.publish()reports whether thebroker wrote the message, and it refuses one when it has no usable session, when a recovery is
pending, or when it is stopped in between - all after the announcement was made. Such an
announcement is one nobody will ever forward, so
pushCommittedChanges()takes it back throughthe announcer, and
DSRSShutdownSync.replicaOfflineMsgNotSent()removes only the entry carryingthat CSN - the two-argument
ConcurrentMap.remove()the forward guard already uses - so that anewer announcement of the same replica is left alone. It wakes the shutdown up as a forward does.
This is the shape #950 proposed, and what "not fixed here: #949" of the earlier revision of this
description was waiting for.
A trade-off worth naming
The grace period is now counted from just before the publish instead of just after it. Normally
that is microseconds. With the send window closed the broker loops on
tryAcquire(500 ms), and aslow publish eats part of the 5 seconds before the message even leaves. The direction is the safe
one - the wait can only end earlier, never later - and
newShutdownDeadline()bounds the wholeshutdown independently.
Tests
PendingChangesTestdrives a realDSRSShutdownSync. The five cases #946 and #976 left thereare kept as they were, and three are new:
theReplicaOfflineMsgIsAnnouncedBeforeItIsPublishedreports the forward from insidepublish(), which is the moment the message reaches the session, so the race is reproducedrather than waited for: with the announcement made afterwards the shutdown is still held back,
with it made before it the forward clears it.
theAnnouncementOfAReplicaOfflineMsgTheBrokerRefusedIsWithdrawnchecks from insidepublish()that the announcement is already in place, refuses the message the way a brokerwith no session does, and asserts nothing holds the shutdown back afterwards - so it pins a
withdrawal, not an announcement which was never made.
theReplicaOfflineMsgHeldBackByAChangeInFlightIsNeverAnnouncedpins the other half: nothing isannounced while a change in flight holds the message back, and nothing is announced when that
change completes either - [#918] Record a ReplicaOfflineMsg as sent only when it really was published #946 gives up on such a message rather than letting it out late.
DSRSShutdownSyncTestgrows three cases for the withdrawal: it ends the wait, it wakes a waitingshutdown up, and the withdrawal of an earlier announcement leaves a newer one of the same replica
alone.
The first new case was watched failing against the ordering this PR changes: moving the
announcement back behind
domain.publish()fails it on the assertion that nothing holds theshutdown back any longer. The second fails without the withdrawal on the same assertion.
Overlaps
for the reason it predicted; its trace, and its giving up on the message which stayed queued,
stay. Its test cases are unchanged.
enable(), which is elsewhere inLDAPReplicationDomainthanthe announcement this branch moves, so the two merged with nothing to reconcile.
13d57e063c). It is the reason thewithdrawal exists: with
domain.publish()reporting a refusal, an announcement made before thepublish has something to be withdrawn for.
pushCommittedChanges()keeps reporting the CSN ofthe message the broker accepted, so
putReplicaOfflineMsg()and the trace behave as [#949] Report a ReplicaOfflineMsg the broker refused as not sent #976 leftthem.
put()records therecipients -
replicaOfflineMsgDispatched()is a no-op without it. This change makes thathold, leaving its
awaitedForwarders == nullfallback for the replica which picked a remotereplication server; the other case that fallback named, an announcement recorded after its
message was relayed, no longer exists, and its comment says so.