From 80481f756d71bd58b4bda627758dcd774e9d5dcd Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 18 Sep 2026 14:45:54 +0000
Subject: [PATCH] [#986] Give back the changes a replay thread the pool stopped had parked (#988)

---
 opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java |   72 ++++++++++++++++++++++++++++++++++++
 1 files changed, 72 insertions(+), 0 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
index 549d35a..3e1f4a9 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
@@ -33,6 +33,7 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
@@ -797,6 +798,77 @@
   }
 
   /**
+   * Runs an action on every replication domain of this server, each of them getting its
+   * turn whatever one of them threw.
+   * <p>
+   * The replay threads are shared by every domain of this server, so what concerns the
+   * pool is done over the domains rather than over the one a replay was last for: a thread
+   * which is stopping gives back what it parked in any of them (issue #986). A throw at one
+   * domain must not leave the ones after it as they were - with changes owned by a thread
+   * which does not exist anymore - so the first failure is thrown once the loop is over,
+   * the others suppressed under it where it records suppression: the error a JVM out of
+   * memory prepared beforehand does not - it was made without its constructor, so it keeps
+   * no list to record them in - and the JVM hands that one out as often as it is asked for
+   * one, so two domains can throw the same instance, and a throwable can not suppress
+   * itself. Recording a failure under the first allocates the list it goes in, so on the
+   * road this loop is for it can be refused in its turn: a failure which can not be
+   * recorded is dropped, and the domains after it still get their turn.
+   * <p>
+   * The iterator over the domains is the one allocation made before the first of them gets
+   * its turn: refused, on the way out of a thread an OutOfMemoryError is ending, it leaves
+   * every domain as it was, and it has no cheaper form. The action itself is not one: a
+   * caller on that road passes an instance it holds rather than one it makes there.
+   * <p>
+   * Not synchronized, and it must not become so: it is called by a replay thread on its way
+   * out, while {@link #stopReplayThreads()} holds the monitor of this class and waits for
+   * that thread to end.
+   *
+   * @param action what is done on each domain; it declares no checked exception, so what
+   *          it throws is an Error or a RuntimeException, and that is what is thrown here
+   */
+  static void forEachDomain(Consumer<LDAPReplicationDomain> action)
+  {
+    Throwable failure = null;
+    for (LDAPReplicationDomain domain : domains.values())
+    {
+      try
+      {
+        action.accept(domain);
+      }
+      catch (Throwable domainFailure)
+      {
+        if (failure == null)
+        {
+          failure = domainFailure;
+        }
+        else if (failure != domainFailure)
+        {
+          try
+          {
+            failure.addSuppressed(domainFailure);
+          }
+          catch (OutOfMemoryError recordRefused)
+          {
+            /*
+             * The list the record goes in could not be allocated: the failure is dropped
+             * rather than allowed to end the loop, since the first one is what is reported
+             * and the domains after this one are still to be visited.
+             */
+          }
+        }
+      }
+    }
+    if (failure instanceof Error)
+    {
+      throw (Error) failure;
+    }
+    if (failure instanceof RuntimeException)
+    {
+      throw (RuntimeException) failure;
+    }
+  }
+
+  /**
    * Gets the number of handled domain objects.
    * @return The number of handled domain objects
    */

--
Gitblit v1.10.0