mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
8 hours ago d0deb0709fc73a94d3d46ac76753933383861b00
[#901] Make the replay retry budget of a replication domain configurable (#944)
9 files modified
430 ■■■■ changed files
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReplicationDomainConfiguration.xml 41 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/02-config.ldif 11 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java 68 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/RemotePendingChanges.java 24 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/replication.properties 4 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/ReplicationTestCase.java 39 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java 186 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/AssuredReplicationPluginTest.java 42 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/DomainFakeCfg.java 15 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReplicationDomainConfiguration.xml
@@ -14,6 +14,7 @@
  Copyright 2007-2010 Sun Microsystems, Inc.
  Portions Copyright 2011-2015 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
  ! -->
<adm:managed-object name="replication-domain"
  plural-name="replication-domains"
@@ -559,4 +560,44 @@
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="replay-give-up-delay">
    <adm:synopsis>
      Specifies how long this directory server retries the replay of a change which its
      backend could not apply, before it gives up on that change and moves on to the
      changes which follow it.
    </adm:synopsis>
    <adm:description>
      The change is asked for again and retried for as long as this budget lasts, over as
      many deliveries as it takes. The budget is a duration rather than a number of
      attempts because a backend which is being imported into, rebuilt or restored serves
      nothing for as long as that operation runs: raise this value before such a
      maintenance operation when it is going to outlast the default. Once the budget is
      spent, the change is recorded as replayed although it never was - this replica then
      diverges from the rest of the topology, raises the
      org.opends.server.replication.UnreplayedChange alert and has to be reinitialized. A
      value of "unlimited" has this server retry the change for as long as it keeps
      failing, which holds the replication of this domain back until an administrator
      intervenes: a change which is retried on and on keeps one of the replay threads this
      server shares between all of its domains busy waiting for it, and has a warning
      logged for every delivery of it, so a domain left like this is paid for by the
      healthy ones too. A value of 0 has this server give up on a change as soon as one
      delivery of it failed, the attempts made in place within that delivery still being
      taken. The budget is measured from the first failure of the change rather than from
      the moment it was set, so lowering it gives up on the next failed delivery of a
      change which has been failing for longer than the new value.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>300000ms</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:duration base-unit="ms" lower-limit="0" allow-unlimited="true" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-replay-give-up-delay</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-server-legacy/resource/schema/02-config.ldif
@@ -15,7 +15,7 @@
# Portions Copyright 2011 profiq, s.r.o.
# Portions Copyright 2012 Manuel Gaupp
# Portions copyright 2015 Edan Idzerda
# Portions copyright 2023-2025 3A Systems LLC
# Portions copyright 2023-2026 3A Systems LLC
# This file contains the attribute type and objectclass definitions for use
# with the Directory Server configuration.
@@ -4106,6 +4106,12 @@
  EQUALITY caseIgnoreMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.60142.2.1.1.1
  NAME 'ds-cfg-replay-give-up-delay'
  EQUALITY caseIgnoreMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler'
  SUP top
@@ -4642,7 +4648,8 @@
        ds-cfg-changetime-heartbeat-interval $
        ds-cfg-log-changenumber $
        ds-cfg-initialization-window-size $
        ds-cfg-source-address )
        ds-cfg-source-address $
        ds-cfg-replay-give-up-delay )
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.58
  NAME 'ds-cfg-length-based-password-validator'
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
@@ -319,17 +319,6 @@
   */
  public static final int IN_PLACE_REPLAY_ATTEMPTS = 10;
  /**
   * How long the replay of a change is retried before this replica gives up on it and
   * moves on to the changes which follow it.
   * <p>
   * The budget is a duration rather than a number of attempts because what
   * {@link #isServerFailure(ResultCode, ResultCode)} reports is measured in minutes: a
   * backend which is being rebuilt, imported into or restored (OPENDJ-49) serves nothing
   * while it works, and a handful of attempts would have this replica give up on every
   * change of a maintenance window it only had to wait out.
   */
  private static final long REPLAY_GIVE_UP_DELAY_IN_MS = 300000;
  /**
   * How long the session is left down before the change is asked for again, multiplied
   * by the number of attempts already made: a backend which keeps failing must not be
   * hammered with a session restart per failed change.
@@ -367,12 +356,6 @@
   */
  private final AtomicInteger consecutiveSessionRestarts = new AtomicInteger();
  /**
   * How long the replay of a change is retried before this replica gives up on it. Only
   * the tests, which can not wait out {@link #REPLAY_GIVE_UP_DELAY_IN_MS}, set another
   * value.
   */
  private volatile long replayGiveUpDelayInMs = REPLAY_GIVE_UP_DELAY_IN_MS;
  /**
   * Serialises the session of this domain being stopped and started again: the replay
   * thread which restarts it after a failed replay must not race the domain being
   * disabled for an import or a restore, or it would bring a broker and a listener
@@ -3017,9 +3000,10 @@
   * The change has deliberately been left out of the ServerState, so the replication
   * server still owns it: restart the session so that it is sent again and replayed on
   * a backend which has hopefully recovered in the meantime. Give up once its replay has
   * been failing for {@link #REPLAY_GIVE_UP_DELAY_IN_MS} and record it as replayed, so
   * that a change which can never be applied here does not stop this replica for good:
   * the administrator is told that this replica has diverged and must be reinitialized.
   * been failing for the {@code replay-give-up-delay} of this domain and record it as
   * replayed, so that a change which can never be applied here does not stop this replica
   * for good: the administrator is told that this replica has diverged and must be
   * reinitialized.
   *
   * @param csn
   *          the CSN of the change which could not be replayed
@@ -3052,7 +3036,20 @@
       */
      return false;
    }
    if (failure.getFailingForMs() >= replayGiveUpDelayInMs)
    /*
     * The budget is read from the configuration at every decision rather than kept in a
     * field of its own: an administrator who raises it because a maintenance window is
     * going to outlast it is not made to restart this server for that, and
     * applyConfigurationChange() replaces the configuration object as a whole. A negative
     * value is the "unlimited" of the duration syntax - this replica then keeps asking for
     * the change rather than ever recording one it did not apply, which is the choice of
     * an operator who would rather have the replication of this domain stop than have it
     * diverge. The generated getter yields the value in the base unit the property is
     * declared with, which is milliseconds here, so it is comparable to what the failure
     * reports as it is.
     */
    final long giveUpDelayInMs = config.getReplayGiveUpDelay();
    if (giveUpDelayInMs >= 0 && failure.getFailingForMs() >= giveUpDelayInMs)
    {
      final LocalizableMessage message = ERR_REPLAY_SKIPPING_CHANGE.get(
          csn, getBaseDN(), failure.getFailingForMs(), failure.getAttempts());
@@ -3230,34 +3227,6 @@
  }
  /**
   * Returns how long the replay of a change is retried before this replica gives up on
   * it.
   * <p>
   * Only there for the tests, which set another value and put this one back.
   *
   * @return how long a change is retried, in milliseconds
   */
  @VisibleForTesting
  public long getReplayGiveUpDelay()
  {
    return replayGiveUpDelayInMs;
  }
  /**
   * Sets how long the replay of a change is retried before this replica gives up on it.
   * <p>
   * Only there for the tests, which can not wait out the {@link
   * #REPLAY_GIVE_UP_DELAY_IN_MS} a backend under maintenance is given.
   *
   * @param delayInMs how long a change is retried, in milliseconds
   */
  @VisibleForTesting
  public void setReplayGiveUpDelay(long delayInMs)
  {
    this.replayGiveUpDelayInMs = delayInMs;
  }
  /**
   * Generate a new CSN and insert it in the pending list.
   *
   * @param operation
@@ -5138,6 +5107,7 @@
    attributes.add("remote-pending-changes-size", remotePendingChanges.getQueueSize());
    attributes.add("dependent-changes-size", remotePendingChanges.getDependentChangesSize());
    attributes.add("changes-in-progress-size", remotePendingChanges.changesInProgressSize());
    attributes.add("changes-with-failed-replay", remotePendingChanges.getFailingChangesSize());
  }
  /**
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/RemotePendingChanges.java
@@ -393,6 +393,30 @@
  }
  /**
   * Returns how many of the listed changes have a failed replay recorded against them.
   * <p>
   * A change is counted from its first failed replay until it leaves this map, whether it
   * leaves it applied or given up on. It is what tells that the replay of this domain is
   * stuck: a domain whose {@code replay-give-up-delay} is unlimited never gives up, so it
   * never counts a failed change either, and the changes it keeps asking for are only
   * visible here.
   *
   * @return the number of listed changes with a failed replay recorded against them
   */
  public int getFailingChangesSize()
  {
    pendingChangesReadLock.lock();
    try
    {
      return failingChanges;
    }
    finally
    {
      pendingChangesReadLock.unlock();
    }
  }
  /**
   * Forgets every change listed here, without updating the ServerState.
   * <p>
   * Called when the domain is disabled: its ServerState is saved and cleared from
opendj-server-legacy/src/messages/org/opends/messages/replication.properties
@@ -620,7 +620,9 @@
 session to the replication server so that it is sent again
ERR_REPLAY_SKIPPING_CHANGE_308=Could not replay change %s in domain "%s": its replay has been \
 failing for %d ms over %d deliveries, each attempted several times in place. The change is being \
 skipped: this replica now diverges from the rest of the topology and must be reinitialized
 skipped: this replica now diverges from the rest of the topology and must be reinitialized. \
 Raise the replay-give-up-delay property of this domain to give a change longer before it is \
 given up on
NOTE_REPLAY_ABANDONED_CHANGE_309=Could not replay change %s in domain "%s": the replay thread \
 it was given to is stopping. The change has not been recorded as replayed and is given back to \
 the replication server, which still owns it
opendj-server-legacy/src/test/java/org/opends/server/replication/ReplicationTestCase.java
@@ -56,6 +56,7 @@
import org.opends.server.core.AddOperation;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
@@ -859,6 +860,44 @@
    return Requests.newModifyRequest(entryDN).addModification(modType, attrName, attrValue);
  }
  /**
   * Applies a modification to the configuration entry of a replication domain, the way an
   * administrator would, and checks that it was applied.
   * <p>
   * The domain reads its configuration for every decision it makes, so a property changed
   * here takes effect on what the domain is doing right now.
   *
   * @param domainConfigDN
   *          the DN of the configuration entry of the domain
   * @param modType
   *          the modification to apply, {@link ModificationType#DELETE} with no value
   *          taking an attribute away so that its property falls back to its default
   * @param attrName
   *          the configuration attribute to modify
   * @param values
   *          the values to set, none when the attribute is being deleted
   */
  protected static void modifyDomainConfig(
      DN domainConfigDN, ModificationType modType, String attrName, String... values)
  {
    final ModifyRequest request = Requests.newModifyRequest(domainConfigDN)
        .addModification(modType, attrName, (Object[]) values);
    final ModifyOperation modOp =
        InternalClientConnection.getRootConnection().processModify(request);
    if (modType == DELETE && modOp.getResultCode() == NO_SUCH_ATTRIBUTE)
    {
      /*
       * The attribute is already gone, which is what the delete was asking for. Said here
       * rather than at the call sites because a delete is how a test puts a property back
       * to its default in a finally: a cleanup which throws would replace the failure it
       * is cleaning up after, and say nothing about it.
       */
      return;
    }
    assertEquals(modOp.getResultCode(), SUCCESS,
        "Cannot " + modType + " " + attrName + " on " + domainConfigDN);
  }
  /** Utility method to create, run a task and check its result. */
  protected void task(String task) throws Exception
  {
opendj-server-legacy/src/test/java/org/opends/server/replication/UpdateOperationTest.java
@@ -45,7 +45,6 @@
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.server.config.server.ReplicationSynchronizationProviderCfg;
import org.opends.server.TestCaseUtils;
@@ -103,8 +102,12 @@
   * How long a change is retried in the tests which check that this replica gives up on
   * a change it can never apply: long enough for the change to be delivered again a
   * couple of times, short enough not to make the test wait out a real backend outage.
   * In the duration syntax of the {@code replay-give-up-delay} property.
   */
  private static final long TEST_GIVE_UP_DELAY_IN_MS = 2000;
  private static final String TEST_GIVE_UP_DELAY = "2000ms";
  /** The configuration attribute which carries the replay give-up budget of a domain. */
  private static final String ATTR_REPLAY_GIVE_UP_DELAY = "ds-cfg-replay-give-up-delay";
  /** An entry with a entryUUID. */
  private Entry personWithUUIDEntry;
@@ -1442,14 +1445,13 @@
      final long initialFailures = getMonitorAttrValue(baseDN, "replayed-updates-failed");
      domain.resetUnreplayedChangeAlertThrottle();
      final int initialAlerts = DummyAlertHandler.getAlertCount(ALERT_TYPE_REPLICATION_UNREPLAYED_CHANGE);
      final long giveUpDelay = domain.getReplayGiveUpDelay();
      // A backend which is down for maintenance is waited out for minutes: this test can
      // not, so the change is given up on after a couple of deliveries instead. The short
      // circuit below is the server's, so it is registered inside the try which takes it
      // back.
      setReplayGiveUpDelay(TEST_GIVE_UP_DELAY);
      try
      {
        // A backend which is down for maintenance is waited out for minutes: this test
        // can not, so the change is given up on after a couple of deliveries instead.
        // Set inside the try which puts it back, like the short circuit below: both are
        // the domain's and the server's for as long as they are left behind.
        domain.setReplayGiveUpDelay(TEST_GIVE_UP_DELAY_IN_MS);
        /*
         * Fail the replay the way a storage failure does: the backend reports it with the
         * server-error-result-code, 80 by default. The short circuit has to be set at the
@@ -1521,7 +1523,7 @@
      finally
      {
        ShortCircuitPlugin.deregisterShortCircuit(OperationType.DELETE, "PreParse");
        domain.setReplayGiveUpDelay(giveUpDelay);
        resetReplayGiveUpDelay();
      }
    }
    finally
@@ -2016,11 +2018,10 @@
      final LDAPReplicationDomain domain = MultimasterReplication.findDomain(baseDN, null);
      final long initialFailures = getMonitorAttrValue(baseDN, "replayed-updates-failed");
      final long giveUpDelay = domain.getReplayGiveUpDelay();
      // Two changes have to be given up on here, so the budget is shortened the same way.
      setReplayGiveUpDelay(TEST_GIVE_UP_DELAY);
      try
      {
        // Both are put back by the finally below, so both are set inside the try.
        domain.setReplayGiveUpDelay(TEST_GIVE_UP_DELAY_IN_MS);
        ShortCircuitPlugin.registerShortCircuit(
            OperationType.DELETE, "PreParse", ResultCode.OTHER.intValue());
@@ -2058,7 +2059,128 @@
      finally
      {
        ShortCircuitPlugin.deregisterShortCircuit(OperationType.DELETE, "PreParse");
        domain.setReplayGiveUpDelay(giveUpDelay);
        resetReplayGiveUpDelay();
      }
    }
    finally
    {
      broker.stop();
    }
  }
  /**
   * Test case for [Issue 901]: a replica whose replay give-up budget is unlimited keeps
   * asking for a change it can not replay instead of ever recording it as replayed, and
   * the budget is read from the configuration for every decision - so lowering it takes
   * effect on the change which is failing right now, without this server being restarted.
   */
  @Test
  public void anUnlimitedReplayGiveUpDelayIsNeverSpent() throws Exception
  {
    testSetUp("anUnlimitedReplayGiveUpDelayIsNeverSpent");
    logger.error(LocalizableMessage.raw("Starting replication test : anUnlimitedReplayGiveUpDelayIsNeverSpent"));
    final int serverId = 18;
    ReplicationBroker broker =
        openReplicationSession(baseDN, serverId, 100, replServerPort, 1000);
    try
    {
      CSNGenerator gen = new CSNGenerator(serverId, 0);
      Entry tmp = TestCaseUtils.addEntry(
          "dn: uid=user.901," + baseDN,
          "objectClass: top",
          "objectClass: person",
          "objectClass: organizationalPerson",
          "objectClass: inetOrgPerson",
          "uid: user.901",
          "cn: Aaccf Amar",
          "sn: Amar");
      String uuid = getEntry(tmp.getName(), 1, true).parseAttribute("entryuuid").asString();
      final LDAPReplicationDomain domain = MultimasterReplication.findDomain(baseDN, null);
      final long initialFailures = getMonitorAttrValue(baseDN, "replayed-updates-failed");
      // An operator who would rather have this domain stop than have it diverge: the
      // change is retried for as long as it keeps failing.
      setReplayGiveUpDelay("unlimited");
      try
      {
        ShortCircuitPlugin.registerShortCircuit(
            OperationType.DELETE, "PreParse", ResultCode.OTHER.intValue());
        final CSN csn = gen.newCSN();
        broker.publish(new DeleteMsg(tmp.getName(), csn, uuid));
        /*
         * One delivery burns IN_PLACE_REPLAY_ATTEMPTS short circuits before it is handed
         * back and the session is restarted, so twice that many of them is a change which
         * was delivered, given back and delivered again - the loop this replica is
         * deliberately left in.
         */
        TestTimer timer = new TestTimer.Builder()
          .maxSleep(60, SECONDS)
          .sleepTimes(100, MILLISECONDS)
          .toTimer();
        timer.repeatUntilSuccess(new CallableVoid()
        {
          @Override
          public void call() throws Exception
          {
            assertTrue(ShortCircuitPlugin.getShortCircuitCount(OperationType.DELETE, "PreParse")
                    > 2 * IN_PLACE_REPLAY_ATTEMPTS,
                "the change was not asked for again while the budget was unlimited");
          }
        });
        assertFalse(domain.getServerState().cover(csn),
            "a replica which never gives up must not record a change it did not apply");
        /*
         * The monitor attribute is the only signal this domain has left: a budget which
         * is never spent raises no alert and counts no failed replay, so the change this
         * replica keeps asking for is visible here and nowhere else. It counts changes
         * rather than deliveries, so the redeliveries above leave it at one.
         */
        assertMonitorAttrValueEventually(baseDN, "changes-with-failed-replay", 1,
            "the change which keeps failing must be counted, once, as a failing change");
        /*
         * The change was delivered at least twice by now, so a counter which was bumped
         * per delivery rather than per change would already be past the value which is
         * being watched: what this asserts is carried by the value itself rather than by
         * how long the window is, which is why the default number of samples is enough
         * here - a window covering a redelivery would have to outlast a backoff which has
         * been climbing since the first failure.
         */
        assertMonitorAttrValueStays(baseDN, "replayed-updates-failed", initialFailures,
            "no change may be counted as failed while the budget is unlimited");
        /*
         * The budget is read for every decision, so the failure which comes next spends
         * this one: the change this replica was holding on to is given up on without the
         * server, or the domain, being restarted for the new value to be seen.
         */
        setReplayGiveUpDelay("0ms");
        TestTimer giveUpTimer = new TestTimer.Builder()
          .maxSleep(120, SECONDS)
          .sleepTimes(200, MILLISECONDS)
          .toTimer();
        giveUpTimer.repeatUntilSuccess(new CallableVoid()
        {
          @Override
          public void call() throws Exception
          {
            assertTrue(domain.getServerState().cover(csn),
                "a budget which was lowered must be spent by the change which is failing");
          }
        });
        assertMonitorAttrValueEventually(baseDN, "replayed-updates-failed", initialFailures + 1,
            "the change which was given up on must be counted once");
        assertMonitorAttrValueEventually(baseDN, "changes-with-failed-replay", 0,
            "a change which was given up on leaves the pending changes and stops being counted");
        assertNotNull(getEntry(tmp.getName(), 1, true), "the entry must not have been deleted");
      }
      finally
      {
        ShortCircuitPlugin.deregisterShortCircuit(OperationType.DELETE, "PreParse");
        resetReplayGiveUpDelay();
      }
    }
    finally
@@ -2543,6 +2665,39 @@
  }
  /**
   * Sets how long the domain of this test retries the replay of a change before it gives
   * up on it, the way an administrator would.
   * <p>
   * A backend under maintenance is waited out for minutes, which no test can afford: the
   * budget is spent over a couple of deliveries instead. The domain reads the property for
   * every decision it makes, so this takes effect on a change which is failing right now,
   * and none of the values it takes stops or starts the session.
   * <p>
   * The domain outlives the test methods, so a test which shortens the budget puts it back
   * with {@link #resetReplayGiveUpDelay()} in a finally, and calls this one before that
   * try: the reset then only ever runs on an attribute which is there to be removed.
   *
   * @param delay
   *          the budget in the duration syntax of the property: {@code 2000ms},
   *          {@code 0ms} to give up as soon as one delivery of the change failed,
   *          {@code unlimited} never to give up on it
   */
  private void setReplayGiveUpDelay(String delay)
  {
    modifyDomainConfig(synchroServerEntry.getName(), REPLACE, ATTR_REPLAY_GIVE_UP_DELAY, delay);
  }
  /**
   * Puts the configured replay give-up budget of the domain of this test back, that is the
   * default of the property: the domain outlives the test methods, so a shortened budget
   * which is left behind is the next test's too.
   */
  private void resetReplayGiveUpDelay()
  {
    modifyDomainConfig(synchroServerEntry.getName(), DELETE, ATTR_REPLAY_GIVE_UP_DELAY);
  }
  /**
   * Enable or disable the receive status of a synchronization provider.
   *
   * @param syncConfigDN The DN of the synchronization provider configuration
@@ -2552,10 +2707,7 @@
   */
  private static void setReceiveStatus(DN syncConfigDN, boolean enable)
  {
    String attrValue = enable ? "TRUE" : "FALSE";
    ModifyRequest request = modifyRequest(syncConfigDN, REPLACE, "ds-cfg-receive-status", attrValue);
    ModifyOperation modOp = getRootConnection().processModify(request);
    assertEquals(modOp.getResultCode(), ResultCode.SUCCESS, "Cannot set receive status");
    modifyDomainConfig(syncConfigDN, REPLACE, "ds-cfg-receive-status", enable ? "TRUE" : "FALSE");
  }
  /**
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/AssuredReplicationPluginTest.java
@@ -211,11 +211,17 @@
  }
  /**
   * Creates a domain using the passed assured settings.
   * Creates a domain using the passed assured settings, plus the passed configuration
   * lines, which are put on the entry the domain is created from.
   * Returns the matching config entry added to the config backend.
   * <p>
   * A property the domain must have from the start belongs here rather than in a
   * configuration change made afterwards: a change stops and starts the session when what
   * it changes calls for it, and the FakeReplicationServer these domains talk to is not
   * built to be reconnected to.
   */
  private Entry createAssuredDomain(AssuredMode assuredMode, int safeDataLevel,
    long assuredTimeout) throws Exception
    long assuredTimeout, String... extraConfigLdifLines) throws Exception
  {
    String baseDn = null;
    switch (assuredMode)
@@ -242,6 +248,10 @@
      // heartbeat = 10 min so no need to emulate heartbeat in fake RS: session
      // not closed by client
     "ds-cfg-changetime-heartbeat-interval: 0ms\n";
    for (String extraLine : extraConfigLdifLines)
    {
      prefixLdif += extraLine + "\n";
    }
    String configEntryLdif = null;
    switch (assuredMode)
@@ -1194,7 +1204,18 @@
        true, testcase);
      replicationServer.start(NO_READ);
      safeReadDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_READ_MODE, 0, TIMEOUT);
      /*
       * A change which keeps failing is asked for again over a restarted session until
       * this replica gives up on it. That is right, and it is not what this test is
       * about: the FakeReplicationServer is not built to be reconnected to, and the
       * restarts would run on while the assertions and the teardown below take their
       * course. A give-up delay of zero has the first failed delivery spend the whole
       * budget, so the change is given up on where it is reported and no session is
       * restarted: the ack of this delivery is published either way - it is sent before
       * the give-up is decided - and the domain settles instead of reconnecting.
       */
      safeReadDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_READ_MODE, 0, TIMEOUT,
          "ds-cfg-replay-give-up-delay: 0ms");
      waitForConnectionToRs(testcase, replicationServer);
      Entry entry = makeEntry(
@@ -1203,9 +1224,6 @@
          "objectClass: organizationalUnit");
      String parentUid = getEntryUUID(DN.valueOf(SAFE_READ_DN));
      final LDAPReplicationDomain domain =
          MultimasterReplication.findDomain(DN.valueOf(SAFE_READ_DN), null);
      final long giveUpDelay = domain.getReplayGiveUpDelay();
      try
      {
        /*
@@ -1219,17 +1237,6 @@
         */
        ShortCircuitPlugin.registerShortCircuit(
            OperationType.ADD, "PreParse", ResultCode.OTHER.intValue());
        /*
         * A change which keeps failing is asked for again over a restarted session until
         * this replica gives up on it. That is right, and it is not what this test is
         * about: the FakeReplicationServer is not built to be reconnected to, and the
         * restarts would run on while the assertions and the teardown below take their
         * course. A give-up delay of zero has the first failure spend the whole budget, so
         * the change is given up on where it is reported and no session is restarted: the
         * ack of this delivery is published either way - it is sent before the give-up is
         * decided - and the domain settles instead of reconnecting.
         */
        domain.setReplayGiveUpDelay(0);
        AckMsg ackMsg = replicationServer.sendAssuredAddMsg(entry, parentUid);
        assertNull(DirectoryServer.getEntry(entry.getName()), "the entry must not have been added");
@@ -1258,7 +1265,6 @@
      }
      finally
      {
        domain.setReplayGiveUpDelay(giveUpDelay);
        ShortCircuitPlugin.deregisterShortCircuit(OperationType.ADD, "PreParse");
      }
    } finally
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/DomainFakeCfg.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2007-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.replication.plugin;
@@ -251,6 +252,20 @@
    return assuredType;
  }
  /**
   * Returns how long the replay of a change is retried before the domain gives up on it.
   * <p>
   * The default the property was given, spelled out here like the other values of this
   * fake configuration: no test varies the budget on a domain built from one.
   *
   * @return the budget in milliseconds
   */
  @Override
  public long getReplayGiveUpDelay()
  {
    return 300000;
  }
  @Override
  public SortedSet<String> getReferralsUrl()
  {