From cebef54070540c6af46671e7dd467ce2a4c61a1c Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 14 Sep 2026 09:15:39 +0000
Subject: [PATCH] [#926] Restart the session of a replication domain in one place, under the lock and the generation (#974)

---
 opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartTest.java                    |  149 ++++++++++++++++++
 opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/LDAPReplicationDomainConfigChangeTest.java |    4 
 opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java                 |   83 +---------
 opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java                    |  219 ++++++++++++++++++++-------
 4 files changed, 321 insertions(+), 134 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
index b94a4dc..537e70c 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java
@@ -369,46 +369,6 @@
    */
   private final AtomicInteger consecutiveSessionRestarts = new AtomicInteger();
   /**
-   * 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
-   * thread back up on a domain which is supposed to be down.
-   * <p>
-   * Holding it costs something, and knowingly: {@code enableService()} connects to the
-   * replication servers under this lock, so a shutdown, an import or a configuration
-   * change which arrives while a replay thread is bringing the session back waits for
-   * that connect - up to the configured connection timeout when the replication servers
-   * are unreachable, which is the same outage that failed the replay. Every one of those
-   * stops the session as its first act, so what they wait for is a session which is about
-   * to be stopped again. The wait between the stop and the start is deliberately left
-   * outside the lock, so the waiting is bounded by a connect rather than by the backoff.
-   * <p>
-   * It comes after the configuration backend's update lock and never before it: a write to
-   * the domain configuration entry holds that lock while it calls
-   * {@link #applyConfigurationChange(ReplicationDomainCfg)}, which takes this one. So
-   * nothing may write a configuration entry while holding this lock - that is why neither
-   * the state {@link #disable()} saves nor the generationId {@link #enable()} stores falls
-   * back to the domain configuration entry when the base entry of the suffix is missing.
-   */
-  private final Object serviceStateLock = new Object();
-  /**
-   * Bumped every time the session of this domain is stopped or started under
-   * {@link #serviceStateLock}. A replay thread which stopped the session only starts it
-   * back if this still is the session it stopped: a configuration change, or the end of
-   * an import, may have started another one while it was waiting for the backend to
-   * recover.
-   * <p>
-   * It does not count the sessions {@code changeConfig()} and {@code readAssuredConfig()}
-   * stop and start, which they do without knowing about it: they run under the lock, so a
-   * replay thread never observes one of theirs, but a session it stopped may well have
-   * been replaced by one of theirs while it was waiting. That is why the guard in
-   * {@link #restartSession(boolean)} reads {@code isListenerShuttingDown()} as well - a
-   * session started outside this counter leaves it untouched, and only the listener says
-   * that one is running.
-   */
-  @GuardedBy("serviceStateLock")
-  private long sessionGeneration;
-  /**
    * Set by {@link #restartService()} when it left the session of this domain alone, so
    * that the configuration change which asked for the restart can say so.
    * <p>
@@ -907,22 +867,21 @@
       return;
     }
 
-    // Disable service if configuration changed
-    final boolean needRestart = needReconnection && allowReconnection;
     /*
      * The session is stopped, the configuration it depends on is changed and the session
      * is started again under the lock which the replay thread restarting the session after
      * a failed replay holds too: a session brought up in the middle of this would be
      * reading a fractional configuration which is half way through being changed. The
-     * pair has to be atomic, which the lock inside disableService()/enableService() does
-     * not make it.
+     * stop, the change and the start have to be atomic together, which taking the lock
+     * inside each of disableService()/enableService() does not make them.
      */
     synchronized (serviceStateLock)
     {
+      // Disable service if configuration changed
+      final boolean needRestart = needReconnection && allowReconnection;
       if (needRestart)
       {
         disableService();
-        sessionGeneration++;
       }
       else if (needReconnection)
       {
@@ -954,7 +913,6 @@
       if (needRestart)
       {
         enableService();
-        sessionGeneration++;
       }
     }
   }
@@ -2525,7 +2483,6 @@
       synchronized (serviceStateLock)
       {
         disableService();
-        sessionGeneration++;
       }
     }
 
@@ -3709,13 +3666,13 @@
     final long stoppedSession;
     synchronized (serviceStateLock)
     {
-      if (shutdown.get() || disabled)
+      if (ownsItsSession())
       {
         // The domain is going away or is being imported into: it owns its session.
         return;
       }
       disableService();
-      stoppedSession = ++sessionGeneration;
+      stoppedSession = getSessionGeneration();
     }
     if (wait)
     {
@@ -3729,21 +3686,18 @@
     }
     synchronized (serviceStateLock)
     {
-      if (shutdown.get() || disabled
-          || sessionGeneration != stoppedSession || !isListenerShuttingDown())
+      if (ownsItsSession() || getSessionGeneration() != stoppedSession)
       {
         /*
          * The domain went away while this thread was waiting, or the session was stopped
          * and started again by something else - a configuration change, the end of an
          * import - in the meantime: the session this thread stopped is gone, so it has
-         * nothing left to start. The generation says a session was started under this
-         * lock; the listener says one is running, which is what a restart made outside it
-         * leaves behind.
+         * nothing left to start. Every stop and every start of a session is counted, so
+         * the generation alone tells one session from another.
          */
         return;
       }
       enableService();
-      sessionGeneration++;
     }
   }
 
@@ -4423,7 +4377,6 @@
        */
       disabled = true;
       disableService(); // This will cut the session and wake up the listener
-      sessionGeneration++;
       awaitReplayDrained();
       state.save();
       state.clearInMemory();
@@ -4591,7 +4544,6 @@
       try
       {
         enableService();
-        sessionGeneration++;
         started = true;
       }
       finally
@@ -5390,23 +5342,6 @@
         domCfg.getBaseDN(), stackTraceToSingleLineString(e));
   }
 
-  /**
-   * {@inheritDoc}
-   * <p>
-   * Taken under {@link #serviceStateLock} like every other configuration change: this one
-   * comes from the external changelog domain - from the entry of its own, or from
-   * {@link #applyECLConfiguration} - and it restarts the session as well.
-   */
-  @Override
-  public void changeConfig(Set<String> includeAttributes,
-      Set<String> includeAttributesForDeletes)
-  {
-    synchronized (serviceStateLock)
-    {
-      super.changeConfig(includeAttributes, includeAttributesForDeletes);
-    }
-  }
-
   @Override
   public boolean isConfigurationChangeAcceptable(
          ReplicationDomainCfg configuration, List<LocalizableMessage> unacceptableReasons)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java
index e2babe2..5538de1 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java
@@ -46,6 +46,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import net.jcip.annotations.GuardedBy;
 import net.jcip.annotations.Immutable;
 
 import org.forgerock.i18n.LocalizableMessage;
@@ -368,6 +369,44 @@
    * session of this ReplicationDomain.
    */
   private final Object sessionLock = new Object();
+  /**
+   * Serialises the stopping and the starting of the session of this domain, so that a
+   * pair of them is atomic: a session stopped so that the configuration it reads can be
+   * changed must not be brought back in the middle of that change by something else.
+   * <p>
+   * Holding it costs something, and knowingly: {@link #enableService()} connects to the
+   * replication servers under this lock, so a shutdown, an import or a configuration
+   * change which arrives while a replay thread is bringing the session back waits for
+   * that connect - up to the configured connection timeout when the replication servers
+   * are unreachable, which is the same outage that failed the replay. Every one of those
+   * stops the session as its first act, so what they wait for is a session which is
+   * about to be stopped again. A wait between a stop and a start belongs outside the
+   * lock, so that the waiting is bounded by a connect rather than by a backoff.
+   * <p>
+   * It comes after the configuration backend's update lock and never before it: a write
+   * to the configuration entry of a domain holds that lock while it calls the domain's
+   * configuration change listener, which takes this one. So nothing may write a
+   * configuration entry while holding this lock - that is why neither the state a domain
+   * saves on its way down nor the generationId it stores on its way up falls back to the
+   * domain configuration entry when the base entry of the suffix is missing.
+   */
+  protected final Object serviceStateLock = new Object();
+  /**
+   * Bumped every time {@link #disableService()} stops the session of this domain or
+   * {@link #enableService()} starts it, both of them under {@link #serviceStateLock}. It
+   * is the identity of the session: a thread which stops one and lets the lock go - a
+   * replay thread waiting out a backoff before it asks for the change it could not apply
+   * again - only starts it back if this still is the session it stopped, since a
+   * configuration change or the end of an import may have started another one while it
+   * was waiting.
+   * <p>
+   * The starts at domain startup are not counted: {@link #startPublishService()} from
+   * the constructor of the domain and {@link #startListenService()} from its start bring
+   * the two halves of the first session up before any replay thread exists to hold a
+   * claim on it.
+   */
+  @GuardedBy("serviceStateLock")
+  private long sessionGeneration;
 
   /**
    * The generationId for this replication domain. It is made of a hash of the
@@ -3294,34 +3333,38 @@
    * It can be useful to disable the Replication Service when the
    * repository where the replicated information is stored becomes
    * temporarily unavailable and replicated updates can therefore not
-   * be replayed during a while. This method is not MT safe.
+   * be replayed during a while.
    */
-  public void disableService()
+  public final void disableService()
   {
-    synchronized (sessionLock)
+    synchronized (serviceStateLock)
     {
-      /*
-       * Stop the broker first in order to prevent the listener from reconnecting - see OPENDJ-457.
-       */
-      if (broker != null)
+      synchronized (sessionLock)
       {
-        broker.stop();
-      }
+        /*
+         * Stop the broker first in order to prevent the listener from reconnecting - see OPENDJ-457.
+         */
+        if (broker != null)
+        {
+          broker.stop();
+        }
 
-      // Stop the listener thread
-      if (listenerThread != null)
-      {
-        listenerThread.initiateShutdown();
-        try
+        // Stop the listener thread
+        if (listenerThread != null)
         {
-          listenerThread.join();
+          listenerThread.initiateShutdown();
+          try
+          {
+            listenerThread.join();
+          }
+          catch (InterruptedException e)
+          {
+            // Give up waiting.
+          }
+          listenerThread = null;
         }
-        catch (InterruptedException e)
-        {
-          // Give up waiting.
-        }
-        listenerThread = null;
       }
+      sessionGeneration++;
     }
   }
 
@@ -3339,6 +3382,24 @@
   }
 
   /**
+   * Returns the generation of the session of this domain: bumped by
+   * {@link #disableService()} and {@link #enableService()} under
+   * {@link #serviceStateLock}, not by the starts at domain startup (see
+   * {@code sessionGeneration}).
+   * <p>
+   * It only says anything while {@link #serviceStateLock} is held, and is meant to be
+   * read under the lock which stopped a session and read again under the lock which
+   * starts it back: the session stopped is gone when the two differ.
+   *
+   * @return the generation of the session of this domain
+   */
+  @GuardedBy("serviceStateLock")
+  protected final long getSessionGeneration()
+  {
+    return sessionGeneration;
+  }
+
+  /**
    * Restart the Replication service after a {@link #disableService()}.
    * <p>
    * The Replication Service will restart from the point indicated by the
@@ -3348,34 +3409,56 @@
    * If some data have changed in the repository during the period of time when
    * the Replication Service was disabled, this {@link ServerState} should
    * therefore be updated by the Replication Domain subclass before calling this
-   * method. This method is not MT safe.
+   * method.
    */
-  public void enableService()
+  public final void enableService()
   {
-    synchronized (sessionLock)
+    synchronized (serviceStateLock)
     {
-      broker.start();
-      startListenService();
+      synchronized (sessionLock)
+      {
+        broker.start();
+        startListenService();
+      }
+      /*
+       * Counted once the session really is up: a start which threw leaves the generation
+       * where it was, so the thread which stopped this session still owns it and may try
+       * to bring it back.
+       */
+      sessionGeneration++;
     }
   }
 
   /**
    * Change some ReplicationDomain parameters.
+   * <p>
+   * The change and the restart it may call for are taken together under
+   * {@link #serviceStateLock}, as {@link #readAssuredConfig(ReplicationDomainCfg, boolean)}
+   * takes its own: a session brought up between the two would negotiate the broker
+   * properties which are half way through being changed.
    *
    * @param config
    *          The new configuration that this domain should now use.
    */
   protected void changeConfig(ReplicationDomainCfg config)
   {
-    if (broker != null && broker.changeConfig(config))
+    synchronized (serviceStateLock)
     {
-      restartService();
+      if (broker != null && broker.changeConfig(config))
+      {
+        restartService();
+      }
     }
   }
 
   /**
    * Applies a configuration change to the attributes which should be included
    * in the ECL.
+   * <p>
+   * Taken under {@link #serviceStateLock} like every other configuration change: this one
+   * comes from the external changelog domain - from the entry of its own, or from the
+   * domain configuration change which reads that entry - and it restarts the session as
+   * well, so the attributes and the restart go together.
    *
    * @param includeAttributes
    *          attributes to be included with all change records.
@@ -3385,11 +3468,14 @@
   public void changeConfig(Set<String> includeAttributes,
       Set<String> includeAttributesForDeletes)
   {
-    final boolean attrsModified = setEclIncludes(
-        getServerId(), includeAttributes, includeAttributesForDeletes);
-    if (attrsModified && broker != null)
+    synchronized (serviceStateLock)
     {
-      restartService();
+      final boolean attrsModified = setEclIncludes(
+          getServerId(), includeAttributes, includeAttributesForDeletes);
+      if (attrsModified && broker != null)
+      {
+        restartService();
+      }
     }
   }
 
@@ -3397,14 +3483,20 @@
    * Stops the session of this domain and starts it again, so that it comes up on the
    * configuration which has just changed.
    * <p>
-   * A subclass may leave it alone: a domain which is shutting down, or which was disabled
-   * for a total update, owns its session and is not given one back by a configuration
-   * change. One which does reports it through {@link #onSessionRestartSuppressed()}.
+   * The pair is taken under {@link #serviceStateLock}, so that nothing starts a session
+   * back between the stop and the start, and both halves are counted by the session
+   * generation. A subclass may leave it alone: a domain which is shutting
+   * down, or which was disabled for a total update, owns its session and is not given one
+   * back by a configuration change. One which does reports it through
+   * {@link #onSessionRestartSuppressed()}.
    */
   protected void restartService()
   {
-    disableService();
-    enableService();
+    synchronized (serviceStateLock)
+    {
+      disableService();
+      enableService();
+    }
   }
 
   /**
@@ -3884,32 +3976,41 @@
   protected void readAssuredConfig(ReplicationDomainCfg config,
       boolean allowReconnection)
   {
-    // Disconnect if required: changing configuration values before
-    // disconnection would make assured replication used immediately and
-    // disconnection could cause some timeouts error.
-    final boolean needReconnection = needReconnection(config);
-    final boolean needRestart = needReconnection && allowReconnection;
-    if (needRestart)
-    {
-      disableService();
-    }
-    else if (needReconnection)
-    {
-      onSessionRestartSuppressed();
-    }
     /*
-     * Stored whether or not the session was restarted for it, as the fractional
-     * configuration is: the assured timeout is the one property a session does not have to
-     * be restarted for, so a change carrying it alone - reported as applied and then
-     * dropped, before - is applied here. A caller which does not allow the reconnection
-     * has no session running assured replication either: the domain is being built, is
-     * shutting down, or is disabled for the length of a total update, and the session its
-     * enable() starts reads what is stored here.
+     * The stop, the change and the start are taken together under serviceStateLock: a
+     * session brought up in between, by a replay thread restarting the session after a
+     * failed replay, would negotiate an assured configuration which is half way through
+     * being changed.
      */
-    assuredConfig = config;
-    if (needRestart)
+    synchronized (serviceStateLock)
     {
-      enableService();
+      // Disconnect if required: changing configuration values before
+      // disconnection would make assured replication used immediately and
+      // disconnection could cause some timeouts error.
+      final boolean needReconnection = needReconnection(config);
+      final boolean needRestart = needReconnection && allowReconnection;
+      if (needRestart)
+      {
+        disableService();
+      }
+      else if (needReconnection)
+      {
+        onSessionRestartSuppressed();
+      }
+      /*
+       * Stored whether or not the session was restarted for it, as the fractional
+       * configuration is: the assured timeout is the one property a session does not have
+       * to be restarted for, so a change carrying it alone - reported as applied and then
+       * dropped, before - is applied here. A caller which does not allow the reconnection
+       * has no session running assured replication either: the domain is being built, is
+       * shutting down, or is disabled for the length of a total update, and the session
+       * its enable() starts reads what is stored here.
+       */
+      assuredConfig = config;
+      if (needRestart)
+      {
+        enableService();
+      }
     }
   }
 
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/LDAPReplicationDomainConfigChangeTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/LDAPReplicationDomainConfigChangeTest.java
index 3d68bf4..50dd017 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/LDAPReplicationDomainConfigChangeTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/LDAPReplicationDomainConfigChangeTest.java
@@ -45,6 +45,7 @@
 import org.opends.server.core.ModifyOperation;
 import org.opends.server.replication.ReplicationTestCase;
 import org.opends.server.replication.common.AssuredMode;
+import org.opends.server.replication.service.ReplicationDomain;
 import org.testng.annotations.Test;
 
 /**
@@ -541,7 +542,8 @@
 
   private static Object serviceStateLockOf(LDAPReplicationDomain domain) throws Exception
   {
-    final Field serviceStateLock = LDAPReplicationDomain.class.getDeclaredField("serviceStateLock");
+    // Declared where the session lives, next to disableService()/enableService()
+    final Field serviceStateLock = ReplicationDomain.class.getDeclaredField("serviceStateLock");
     serviceStateLock.setAccessible(true);
     return serviceStateLock.get(domain);
   }
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartTest.java
new file mode 100644
index 0000000..04a9e5a
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/SessionRestartTest.java
@@ -0,0 +1,149 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.replication.plugin;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.opends.server.TestCaseUtils.*;
+import static org.testng.Assert.*;
+
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.forgerock.opendj.config.server.ConfigChangeResult;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.replication.ReplicationTestCase;
+import org.opends.server.replication.server.ReplServerFakeConfiguration;
+import org.opends.server.replication.server.ReplicationServer;
+import org.testng.annotations.Test;
+
+/**
+ * Tests that a configuration change does not start the session of a domain which stopped
+ * its own session: a domain which is shutting down, or whose data is being replaced, owns
+ * its session and is the one which brings it back.
+ */
+@SuppressWarnings("javadoc")
+public class SessionRestartTest extends ReplicationTestCase
+{
+  private static final int RS_ID = 601;
+  private static final int DS_ID = 1;
+  private static final int GROUP_ID = 1;
+
+  @Test
+  public void aConfigurationChangeDoesNotStartTheSessionOfADisabledDomain() throws Exception
+  {
+    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
+    ReplicationServer replicationServer = null;
+    LDAPReplicationDomain domain = null;
+    try
+    {
+      final int rsPort = TestCaseUtils.findFreePort();
+      replicationServer = createReplicationServer(rsPort, "sessionRestartTestDisabledDb");
+
+      final DomainFakeCfg domainCfg = newDomainCfg(baseDN, rsPort);
+      domain = MultimasterReplication.createNewDomain(domainCfg);
+      domain.start();
+      assertTrue(domain.isConnected());
+
+      // The data this domain replicates is about to be replaced by an import or a restore.
+      domain.disable();
+      assertFalse(domain.isConnected());
+
+      changeEclIncludes(domain, domainCfg);
+
+      assertFalse(domain.isConnected(),
+          "a configuration change started the session of a disabled domain");
+    }
+    finally
+    {
+      if (domain != null)
+      {
+        MultimasterReplication.deleteDomain(baseDN);
+      }
+      remove(replicationServer);
+    }
+  }
+
+  @Test
+  public void aConfigurationChangeDoesNotStartTheSessionOfAShutDownDomain() throws Exception
+  {
+    final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING);
+    ReplicationServer replicationServer = null;
+    LDAPReplicationDomain domain = null;
+    try
+    {
+      final int rsPort = TestCaseUtils.findFreePort();
+      replicationServer = createReplicationServer(rsPort, "sessionRestartTestShutdownDb");
+
+      final DomainFakeCfg domainCfg = newDomainCfg(baseDN, rsPort);
+      domain = MultimasterReplication.createNewDomain(domainCfg);
+      domain.start();
+      assertTrue(domain.isConnected());
+
+      domain.shutdown();
+      assertFalse(domain.isConnected());
+
+      changeEclIncludes(domain, domainCfg);
+
+      assertFalse(domain.isConnected(),
+          "a configuration change started the session of a domain which has shut down");
+    }
+    finally
+    {
+      if (domain != null)
+      {
+        MultimasterReplication.deleteDomain(baseDN);
+      }
+      remove(replicationServer);
+    }
+  }
+
+  /**
+   * Applies a configuration change which changes the attributes the external changelog
+   * includes. The domain hands it to {@code ExternalChangelogDomain}, which asks for the
+   * session to be restarted so that the replication server hears the new list.
+   */
+  private void changeEclIncludes(LDAPReplicationDomain domain, DomainFakeCfg domainCfg)
+      throws Exception
+  {
+    final SortedSet<String> eclIncludes = new TreeSet<>();
+    eclIncludes.add("cn");
+    domainCfg.setExternalChangelogDomain(
+        new ExternalChangelogDomainFakeCfg(true, eclIncludes, new TreeSet<String>()));
+
+    final ConfigChangeResult ccr = domain.applyConfigurationChange(domainCfg);
+    assertEquals(ccr.getResultCode(), ResultCode.SUCCESS, ccr.getMessages().toString());
+    // the restart the change asked for was refused, and said so rather than reported as applied
+    assertTrue(ccr.adminActionRequired(), "the refused restart was reported as fully applied");
+    // the change did reach the external changelog configuration of the domain
+    assertThat(domain.getEclIncludes()).contains("cn");
+  }
+
+  private DomainFakeCfg newDomainCfg(DN baseDN, int rsPort)
+  {
+    final SortedSet<String> replServers = new TreeSet<>();
+    replServers.add("localhost:" + rsPort);
+    return new DomainFakeCfg(baseDN, DS_ID, replServers, GROUP_ID);
+  }
+
+  private ReplicationServer createReplicationServer(int rsPort, String dbDir) throws Exception
+  {
+    final ReplServerFakeConfiguration conf = new ReplServerFakeConfiguration(
+        rsPort, dbDir, 0, RS_ID, 0, 100, new TreeSet<String>(), GROUP_ID, 1000, 5000);
+    return new ReplicationServer(conf);
+  }
+}

--
Gitblit v1.10.0