Skip to content

Commit e8c0d1b

Browse files
authored
[#710] Fix replication catch-up re-sending updates with the original assured flag (#714)
1 parent 0918c2e commit e8c0d1b

9 files changed

Lines changed: 362 additions & 9 deletions

File tree

opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/AckMsg.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2006-2009 Sun Microsystems, Inc.
1515
* Portions Copyright 2013-2015 ForgeRock AS.
16+
* Portions Copyright 2026 3A Systems, LLC
1617
*/
1718
package org.opends.server.replication.protocol;
1819

@@ -254,5 +255,11 @@ public String errorsToString()
254255
"concerned server ids: " + idList;
255256
}
256257

258+
/** {@inheritDoc} */
259+
@Override
260+
public String toString()
261+
{
262+
return getClass().getSimpleName() + " csn: " + csn + ", " + errorsToString();
263+
}
257264
}
258265

opendj-server-legacy/src/main/java/org/opends/server/replication/server/ExpectedAcksInfo.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@
1313
*
1414
* Copyright 2008-2009 Sun Microsystems, Inc.
1515
* Portions Copyright 2013-2015 ForgeRock AS.
16+
* Portions Copyright 2026 3A Systems, LLC
1617
*/
1718

1819
package org.opends.server.replication.server;
1920

21+
import java.util.Collections;
2022
import java.util.HashMap;
23+
import java.util.HashSet;
2124
import java.util.List;
2225
import java.util.Map;
26+
import java.util.Set;
2327

2428
import org.opends.server.replication.common.AssuredMode;
2529
import org.opends.server.replication.common.CSN;
@@ -68,6 +72,15 @@ public abstract class ExpectedAcksInfo
6872
*/
6973
protected Map<Integer,Boolean> expectedServersAckStatus = new HashMap<>();
7074

75+
/**
76+
* Immutable snapshot of the ids of the servers we expect an ack from, taken
77+
* at construction time. Used for lock-free membership checks: the key set of
78+
* {@link #expectedServersAckStatus} never changes after construction, but
79+
* that map has its values mutated under lock by {@code processReceivedAck()},
80+
* so it must not be read concurrently without synchronization.
81+
*/
82+
private final Set<Integer> expectedServerIds;
83+
7184
/**
7285
* Facility for monitoring:
7386
* If the timeout occurs for the original update, we call createAck(true)
@@ -99,6 +112,22 @@ protected ExpectedAcksInfo(CSN csn, ServerHandler requesterServerHandler,
99112
{
100113
expectedServersAckStatus.put(serverId, false);
101114
}
115+
this.expectedServerIds =
116+
Collections.unmodifiableSet(new HashSet<>(expectedServers));
117+
}
118+
119+
/**
120+
* Indicates whether the provided server is one of the servers an ack is
121+
* expected from for the matching update message. Reads an immutable snapshot
122+
* taken at construction time, so it is safe to call without holding the lock
123+
* on this object.
124+
*
125+
* @param serverId The serverId of the server.
126+
* @return true if an ack is expected from the provided server.
127+
*/
128+
public boolean isExpectedServer(int serverId)
129+
{
130+
return expectedServerIds.contains(serverId);
102131
}
103132

104133
/**

opendj-server-legacy/src/main/java/org/opends/server/replication/server/MessageHandler.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ class MessageHandler extends MonitorProvider<MonitorProviderCfg>
8080
private final int maxQueueBytesSize;
8181
/** Specifies whether the consumer is following the producer (is not late). */
8282
private boolean following;
83+
/**
84+
* Specifies whether the last update message returned by
85+
* {@link #getNextMessage()} was re-read from the changelog DB (catch-up
86+
* path) rather than taken from the in-memory {@link #msgQueue}. Only ever
87+
* accessed by the single consumer thread calling {@link #getNextMessage()}.
88+
*/
89+
private boolean lastMessageFromLateQueue;
8390
/** Specifies the current serverState of this handler. */
8491
private ServerState serverState;
8592
/** Specifies the baseDN of the domain. */
@@ -225,6 +232,21 @@ boolean isFollowing()
225232
}
226233
}
227234

235+
/**
236+
* Indicates whether the last update message returned by
237+
* {@link #getNextMessage()} was re-read from the changelog DB (catch-up
238+
* path) rather than taken from the in-memory queue.
239+
* <p>
240+
* Must only be called from the consumer thread calling
241+
* {@link #getNextMessage()}.
242+
*
243+
* @return true if the last returned update message came from the late queue
244+
*/
245+
protected boolean isLastMessageFromLateQueue()
246+
{
247+
return lastMessageFromLateQueue;
248+
}
249+
228250
/**
229251
* Retrieves the name of this monitor provider. It should be unique among all
230252
* monitor providers, including all instances of the same monitor provider.
@@ -319,6 +341,9 @@ protected UpdateMsg getNextMessage() throws ChangelogException
319341
msgQueue.consumeUpTo(msg);
320342
if (updateServerState(msg))
321343
{
344+
// the returned instance is the one re-read from the
345+
// changelog DB, not its msgQueue equivalent
346+
lastMessageFromLateQueue = true;
322347
return msg;
323348
}
324349
}
@@ -347,6 +372,7 @@ protected UpdateMsg getNextMessage() throws ChangelogException
347372
}
348373
if (updateServerState(msg))
349374
{
375+
lastMessageFromLateQueue = true;
350376
return msg;
351377
}
352378
continue;
@@ -379,6 +405,7 @@ protected UpdateMsg getNextMessage() throws ChangelogException
379405
* by the other server.
380406
* Otherwise just loop to select the next message.
381407
*/
408+
lastMessageFromLateQueue = false;
382409
return msg;
383410
}
384411
}

opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationServerDomain.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*
1414
* Copyright 2006-2010 Sun Microsystems, Inc.
1515
* Portions Copyright 2011-2016 ForgeRock AS.
16-
* Portions Copyrighted 2026 3A Systems, LLC.
16+
* Portions Copyright 2026 3A Systems, LLC
1717
*/
1818
package org.opends.server.replication.server;
1919

@@ -789,6 +789,20 @@ private boolean isDifferentGenerationId(long generationId)
789789
return this.generationId > 0 && this.generationId != generationId;
790790
}
791791

792+
/**
793+
* Indicates whether the ack window for the provided CSN is still open and
794+
* the provided server is one of the servers an ack is expected from.
795+
*
796+
* @param csn The CSN of the update message.
797+
* @param serverId The serverId of the candidate acknowledging server.
798+
* @return true if an ack from the provided server would be accounted for.
799+
*/
800+
boolean isExpectedAck(CSN csn, int serverId)
801+
{
802+
final ExpectedAcksInfo expectedAcksInfo = waitingAcks.get(csn);
803+
return expectedAcksInfo != null && expectedAcksInfo.isExpectedServer(serverId);
804+
}
805+
792806
/**
793807
* Process an ack received from a given server.
794808
*

opendj-server-legacy/src/main/java/org/opends/server/replication/server/SafeDataExpectedAcksInfo.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2008-2009 Sun Microsystems, Inc.
1515
* Portions Copyright 2013-2015 ForgeRock AS.
16+
* Portions Copyright 2026 3A Systems, LLC
1617
*/
1718
package org.opends.server.replication.server;
1819

@@ -84,7 +85,19 @@ public boolean processReceivedAck(ServerHandler ackingServer, AckMsg ackMsg)
8485

8586
// Get the ack status for the matching server
8687
int ackingServerId = ackingServer.getServerId();
87-
boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
88+
Boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
89+
if (ackReceived == null)
90+
{
91+
// Ack from a server we were not expecting an ack from, for instance
92+
// because the update was delivered to it with the assured flag of the
93+
// original sender through the changelog catch-up path: ignore it.
94+
if (logger.isTraceEnabled())
95+
{
96+
logger.trace("Received ack from not expected server id: " +
97+
ackingServerId + " ack message: " + ackMsg);
98+
}
99+
return false;
100+
}
88101
if (ackReceived)
89102
{
90103
// Sanity check: this should never happen

opendj-server-legacy/src/main/java/org/opends/server/replication/server/SafeReadExpectedAcksInfo.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2008-2009 Sun Microsystems, Inc.
1515
* Portions Copyright 2013-2015 ForgeRock AS.
16+
* Portions Copyright 2026 3A Systems, LLC
1617
*/
1718
package org.opends.server.replication.server;
1819

@@ -146,7 +147,19 @@ public boolean processReceivedAck(ServerHandler ackingServer, AckMsg ackMsg)
146147
{
147148
// Get the ack status for the matching server
148149
int ackingServerId = ackingServer.getServerId();
149-
boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
150+
Boolean ackReceived = expectedServersAckStatus.get(ackingServerId);
151+
if (ackReceived == null)
152+
{
153+
// Ack from a server we were not expecting an ack from, for instance
154+
// because the update was delivered to it with the assured flag of the
155+
// original sender through the changelog catch-up path: ignore it.
156+
if (logger.isTraceEnabled())
157+
{
158+
logger.trace("Received ack from not expected server id: "
159+
+ ackingServerId + " ack message: " + ackMsg);
160+
}
161+
return false;
162+
}
150163
if (ackReceived)
151164
{
152165
// Sanity check: this should never happen

opendj-server-legacy/src/main/java/org/opends/server/replication/server/ServerHandler.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
*
1414
* Copyright 2006-2010 Sun Microsystems, Inc.
1515
* Portions Copyright 2011-2016 ForgeRock AS.
16+
* Portions Copyright 2026 3A Systems, LLC
1617
*/
1718
package org.opends.server.replication.server;
1819

1920
import static org.opends.messages.ReplicationMessages.*;
21+
import static org.opends.server.util.StaticUtils.*;
2022

2123
import java.io.IOException;
24+
import java.io.UnsupportedEncodingException;
2225
import java.util.Random;
2326
import java.util.concurrent.Semaphore;
2427
import java.util.concurrent.TimeUnit;
@@ -924,12 +927,32 @@ public void shutdown()
924927
*/
925928
public UpdateMsg take() throws ChangelogException
926929
{
927-
final UpdateMsg msg = getNextMessage();
930+
UpdateMsg msg = getNextMessage();
931+
final boolean fromLateQueue = isLastMessageFromLateQueue();
928932

929933
acquirePermitInSendWindow();
930934

931935
if (msg != null)
932936
{
937+
// Updates re-read from the changelog DB (catch-up path) carry the
938+
// assured flag, mode and safe data level of their original sender:
939+
// the NotAssuredUpdateMsg substitution performed at publish time by
940+
// ReplicationServerDomain.addUpdate() only exists on the in-memory
941+
// queue path. Normalize them here: keep the assured flag only while
942+
// the ack window is still open and this server is expected to ack.
943+
// Messages taken from the in-memory queue already carry the
944+
// publish-time decision and must NOT be revisited: the ack window may
945+
// legitimately close (timeout, or enough acks already received)
946+
// before a slow peer gets here - e.g. when acquirePermitInSendWindow()
947+
// above blocks on a closed send window - and such a peer must still
948+
// receive the assured flag it was deemed eligible for. Its late ack is
949+
// then safely ignored by ReplicationServerDomain.processAck() and
950+
// ExpectedAcksInfo.processReceivedAck().
951+
if (fromLateQueue && msg.isAssured()
952+
&& !replicationServerDomain.isExpectedAck(msg.getCSN(), serverId))
953+
{
954+
msg = toNotAssuredUpdateMsg(msg);
955+
}
933956
incrementOutCount();
934957
if (msg.isAssured())
935958
{
@@ -940,6 +963,37 @@ public UpdateMsg take() throws ChangelogException
940963
return null;
941964
}
942965

966+
/**
967+
* Substitutes a not assured version of the provided update message so that a
968+
* peer not expected to acknowledge it does not receive it with the assured
969+
* flag.
970+
* <p>
971+
* This is the counterpart, for the changelog catch-up path, of the
972+
* {@link NotAssuredUpdateMsg} substitution performed on the in-memory queue
973+
* path by ReplicationServerDomain.addUpdate(): updates re-read from the
974+
* changelog DB keep the assured flag of their original sender and must be
975+
* normalized in {@link #take()} before being handed to the ServerWriter.
976+
*/
977+
private UpdateMsg toNotAssuredUpdateMsg(UpdateMsg msg)
978+
{
979+
try
980+
{
981+
return new NotAssuredUpdateMsg(msg);
982+
}
983+
catch (UnsupportedEncodingException e)
984+
{
985+
// Could not build the not assured form (unexpected message encoding).
986+
// Deliver the original message rather than dropping it: losing the
987+
// update would break replication consistency, which is worse than a
988+
// spurious ack - and such an ack is now safely ignored by
989+
// ExpectedAcksInfo.processReceivedAck().
990+
logger.error(LocalizableMessage.raw(
991+
"Could not substitute a not assured version of update message %s: %s",
992+
msg, stackTraceToSingleLineString(e)));
993+
return msg;
994+
}
995+
}
996+
943997
private void acquirePermitInSendWindow()
944998
{
945999
boolean acquired = false;

0 commit comments

Comments
 (0)