From 5bd63c0f087d5e0b8e44910301a69ad9481a71e0 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 05 Aug 2026 10:12:59 +0000
Subject: [PATCH] [#841] Fix flaky InitOnLineTest: notify the requester when a remotely requested export cannot start (#845)

---
 opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationDomain.java |   83 +++++++++++++++++++++++++++++++++--------
 1 files changed, 66 insertions(+), 17 deletions(-)

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 68e2999..cc05d77 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
@@ -1469,30 +1469,78 @@
     // subsequent total update as a simultaneous import/export.
     final Map<Integer, DSInfo> replicaInfos = getReplicaInfos();
     final DSInfo targetDsi;
-    if (serverToInitialize == RoutableMsg.ALL_SERVERS)
+    final ImportExportContext ieCtx;
+    final long entryCount;
+    try
     {
-      if (replicaInfos.isEmpty())
+      if (serverToInitialize == RoutableMsg.ALL_SERVERS)
       {
-        throw new DirectoryException(UNWILLING_TO_PERFORM,
-            ERR_FULL_UPDATE_NO_REMOTES.get(getBaseDN(), getServerId()));
+        if (replicaInfos.isEmpty())
+        {
+          throw new DirectoryException(UNWILLING_TO_PERFORM,
+              ERR_FULL_UPDATE_NO_REMOTES.get(getBaseDN(), getServerId()));
+        }
+        targetDsi = null;
       }
-      targetDsi = null;
+      else
+      {
+        targetDsi = getDsInfoOrNull(replicaInfos.values(), serverToInitialize);
+        if (targetDsi == null)
+        {
+          throw new DirectoryException(UNWILLING_TO_PERFORM,
+              ERR_FULL_UPDATE_MISSING_REMOTE.get(getBaseDN(), getServerId(), serverToInitialize));
+        }
+      }
+
+      // countEntries() would otherwise first be called by
+      // initializeRemote(ieCtx, ...) outside the region that reports the
+      // failure to the requester: probe it here so a backend that cannot be
+      // exported is notified like any other rejection.
+      entryCount = countEntries();
+
+      ieCtx = acquireIEContext(false);
     }
-    else
+    catch (DirectoryException de)
     {
-      targetDsi = getDsInfoOrNull(replicaInfos.values(), serverToInitialize);
-      if (targetDsi == null)
+      if (initTask == null
+          && serverToInitialize != RoutableMsg.ALL_SERVERS
+          && serverRunningTheTask != getServerId())
       {
-        throw new DirectoryException(UNWILLING_TO_PERFORM,
-            ERR_FULL_UPDATE_MISSING_REMOTE.get(getBaseDN(), getServerId(), serverToInitialize));
+        /*
+        The export was requested by the remote server itself (the
+        ExportTask contract: no local task and the requester is the
+        target), which has acquired an import context and is now waiting
+        for the InitializeTargetMsg: without a reply it would wait forever
+        (e.g. when this request raced the topology propagation and the
+        requester is not in our replicas view yet). Best effort: the
+        requester may not even be routable in that very case - the
+        replication server then bounces the notification back as an
+        ErrorMsg(ERR_NO_REACHABLE_PEER) applied to whatever import/export
+        context is live here (ErrorMsg carries no correlation id) - and
+        when the session is down the requester detects the disconnection
+        instead.
+        */
+        logger.info(NOTE_FULL_UPDATE_REMOTE_REQUEST_REJECTED,
+            getBaseDN(), getServerId(), serverToInitialize, de.getMessageObject());
+        try
+        {
+          if (broker.isConnected())
+          {
+            broker.publish(new ErrorMsg(serverToInitialize, de.getMessageObject()));
+          }
+        }
+        catch (Exception e)
+        {
+          // Ignore the failure raised while notifying the root failure
+        }
       }
+      throw de;
     }
 
-    final ImportExportContext ieCtx = acquireIEContext(false);
     try
     {
       initializeRemote(ieCtx, replicaInfos, targetDsi, serverToInitialize,
-          serverRunningTheTask, initTask, initWindow);
+          serverRunningTheTask, initTask, initWindow, entryCount);
     }
     finally
     {
@@ -1505,17 +1553,18 @@
 
   /**
    * Performs the remote initialization with the import/export context already
-   * acquired - and released - by the caller.
+   * acquired - and released - by the caller, which also counted the entries
+   * to export while validating the request.
    */
   private void initializeRemote(ImportExportContext ieCtx,
       Map<Integer, DSInfo> replicaInfos, DSInfo targetDsi,
       int serverToInitialize, int serverRunningTheTask, Task initTask,
-      int initWindow) throws DirectoryException
+      int initWindow, long entryCount) throws DirectoryException
   {
     if (serverToInitialize == RoutableMsg.ALL_SERVERS)
     {
       logger.info(NOTE_FULL_UPDATE_ENGAGED_FOR_REMOTE_START_ALL,
-          countEntries(), getBaseDN(), getServerId());
+          entryCount, getBaseDN(), getServerId());
 
       ieCtx.startList.addAll(replicaInfos.keySet());
 
@@ -1529,7 +1578,7 @@
     }
     else
     {
-      logger.info(NOTE_FULL_UPDATE_ENGAGED_FOR_REMOTE_START, countEntries(),
+      logger.info(NOTE_FULL_UPDATE_ENGAGED_FOR_REMOTE_START, entryCount,
           getBaseDN(), getServerId(), serverToInitialize);
 
       ieCtx.startList.add(serverToInitialize);
@@ -1550,7 +1599,7 @@
         {
           ieCtx.initializeTask = initTask;
         }
-        ieCtx.initializeCounters(countEntries());
+        ieCtx.initializeCounters(entryCount);
         ieCtx.msgCnt = 0;
         ieCtx.initNumLostConnections = broker.getNumLostConnections();
         ieCtx.initWindow = initWindow;

--
Gitblit v1.10.0