From c2f7a14064a1bcd170eb25cf8ff7ec57df132dbd Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 22 Sep 2026 10:29:51 +0000
Subject: [PATCH] [#1040] Leave the session to a total update when a configuration change asks for a restart (#1042)

---
 opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/LDAPReplicationDomain.java |   54 ++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 40 insertions(+), 14 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 6ec3909..1467c35 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
@@ -6158,8 +6158,9 @@
         changeConfig(configuration);
 
         // Read assured + fractional configuration and each time reconnect if needed. A
-        // domain which owns its session gets none of those reconnections.
-        final boolean allowReconnection = !ownsItsSession();
+        // session which has an owner - the domain itself, or a total update into this
+        // replica - gets none of those reconnections.
+        final boolean allowReconnection = !sessionHasAnOwner();
         readAssuredConfig(configuration, allowReconnection);
         readFractionalConfig(configuration, allowReconnection);
         solveConflictFlag = isSolveConflict(configuration);
@@ -6207,9 +6208,10 @@
   }
 
   /**
-   * Whether the session of this domain has an owner other than the replay thread which
-   * would restart it after a failed replay: the domain itself, when it is shutting down or
-   * disabled ({@link #ownsItsSession()}), or a total update into this replica.
+   * Whether the session of this domain has an owner other than the thread which would
+   * restart it - a replay thread after a failed replay, or a configuration change for what
+   * it carries: the domain itself, when it is shutting down or disabled
+   * ({@link #ownsItsSession()}), or a total update into this replica.
    * <p>
    * The total update owns the session from the moment it is asked for, not from the
    * moment its entries stream: the {@code InitializeTargetMsg} which answers the request
@@ -6223,6 +6225,15 @@
    * Listed, it holds the ServerState back as well: a commit moves the state no further than
    * the oldest uncommitted change, so the state in memory, and the one persisted from it,
    * stop at the change until that restart.
+   * <p>
+   * A configuration change is refused while a total update runs - by the listener of the
+   * domain entry and by the one of its external changelog entry - so what reaches the
+   * domain all the same is a change accepted before the total update was asked for. The
+   * restart it asks for is refused on this predicate too, and the session the import
+   * starts when it ends reads the configuration stored meanwhile. Made through the server
+   * configuration, that restart would not end: it waits for the listener thread, which is
+   * the import, and the import waits for the lock of the configuration the change holds,
+   * to enable the backend back once the stream it was reading ends.
    */
   private boolean sessionHasAnOwner()
   {
@@ -6234,21 +6245,30 @@
   {
     synchronized (serviceStateLock)
     {
-      if (ownsItsSession())
+      if (sessionHasAnOwner())
       {
         /*
-         * The domain is going away or is being imported into: a restart here would bring
-         * a session, and the listener thread which goes with it, back up on a domain
-         * whose ServerState is gone from memory. The session started when the domain is
-         * enabled again reads the configuration this restart was asked for.
+         * The domain is going away or is disabled: a restart here would bring a session,
+         * and the listener thread which goes with it, back up on a domain whose
+         * ServerState is gone from memory. The session started when the domain is enabled
+         * again reads the configuration this restart was asked for.
+         *
+         * Or a total update into this replica is reading the session: the import streams
+         * over it, on the listener thread a restart would stop and wait for. Stopped, the
+         * broker ends the stream on the entries which had arrived; waited for, the listener
+         * thread ends the import and enables the backend back through the server
+         * configuration - whose lock a change made through it holds while it waits. The
+         * import starts the next session itself when it ends, from the state it loaded,
+         * and that session reads the configuration this restart was asked for.
          *
          * Recorded rather than passed over in silence: the configuration a restart was
          * asked for is stored, and it is the session which is not brought up on it, so a
          * change which reports plain success would have the administrator believe the
          * domain is running on it already. A domain disabled for a total update comes up
-         * on it when the total update ends; one which stays disabled - enable() gives up
-         * when the data state it reads cannot be loaded, and nothing calls it again -
-         * never does, and that is what the administrator is told to act on.
+         * on it when the total update ends, and so does one being imported into; one which
+         * stays disabled - enable() gives up when the data state it reads cannot be
+         * loaded, and nothing calls it again - never does, and that is what the
+         * administrator is told to act on.
          */
         onSessionRestartSuppressed();
         return;
@@ -6299,7 +6319,13 @@
   public boolean isConfigurationChangeAcceptable(
          ReplicationDomainCfg configuration, List<LocalizableMessage> unacceptableReasons)
   {
-    // Check that a import/export is not in progress
+    /*
+     * Check that a import/export is not in progress. The listener of the external
+     * changelog entry of this domain refuses its change for the same reason: what either
+     * change restarts the session for, an import into this replica is reading over that
+     * session. One which starts between this check and the change being applied meets
+     * the restart guard instead (see sessionHasAnOwner()).
+     */
     if (ieRunning())
     {
       unacceptableReasons.add(

--
Gitblit v1.10.0