From e86f702f8e11d85eb7fcbde8de64657a4e308a21 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 11 Sep 2026 13:06:49 +0000
Subject: [PATCH] [#966] Tell the import and restore task listeners the task is over on every path (#969)

---
 opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java |   79 +++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java          |   68 ++++++++++------
 opendj-server-legacy/src/main/java/org/opends/server/tasks/RestoreTask.java         |   49 +++++++-----
 3 files changed, 149 insertions(+), 47 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java b/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
index 5d69679..aaebe55 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tasks/ImportTask.java
@@ -627,21 +627,29 @@
     // and to take appropriate actions.
     DirectoryServer.notifyImportBeginning(backend, importConfig);
 
-    // Disable the backend.
+    /*
+     * From here the listeners must be told that the import is over whichever way this
+     * method returns: a listener which took something offline when it began - a
+     * replication domain disables itself - gets no other chance to put it back.
+     */
+    boolean backendDisabled = false;
+    boolean successful = false;
     try
     {
-      TaskUtils.disableBackend(backend.getBackendID());
-    }
-    catch (DirectoryException e)
-    {
-      logger.traceException(e);
+      // Disable the backend.
+      try
+      {
+        TaskUtils.disableBackend(backend.getBackendID());
+        backendDisabled = true;
+      }
+      catch (DirectoryException e)
+      {
+        logger.traceException(e);
 
-      logger.error(e.getMessageObject());
-      return TaskState.STOPPED_BY_ERROR;
-    }
+        logger.error(e.getMessageObject());
+        return TaskState.STOPPED_BY_ERROR;
+      }
 
-    try
-    {
       // Acquire an exclusive lock for the backend.
       try
       {
@@ -665,12 +673,12 @@
       try
       {
         backend.importLDIF(importConfig, DirectoryServer.getInstance().getServerContext());
+        successful = true;
       }
       catch (DirectoryException de)
       {
         logger.traceException(de);
 
-        DirectoryServer.notifyImportEnded(backend, importConfig, false);
         if (de.getResultCode().equals(DirectoryServer.getCoreConfigManager().getServerErrorResultCode()))
         {
           logger.error(ERR_LDIFIMPORT_ERROR_DURING_IMPORT.get(de.getMessageObject()));
@@ -685,7 +693,6 @@
       {
         logger.traceException(e);
 
-        DirectoryServer.notifyImportEnded(backend, importConfig, false);
         logger.error(ERR_LDIFIMPORT_ERROR_DURING_IMPORT, getExceptionMessage(e));
         return TaskState.STOPPED_BY_ERROR;
       }
@@ -713,23 +720,32 @@
     }
     finally
     {
-      // Enable the backend.
-      try
+      // Enable the backend, if it was this task which disabled it.
+      boolean backendLeftDisabled = false;
+      if (backendDisabled)
       {
-        TaskUtils.enableBackend(backend.getBackendID());
-        // It is necessary to retrieve the backend structure again
-        // because disabling and enabling it again may have resulted
-        // in a new backend being registered to the server.
-        backend = getServerContext().getBackendConfigManager().getLocalBackendById(backend.getBackendID());
-      }
-      catch (DirectoryException e)
-      {
-        logger.traceException(e);
+        try
+        {
+          TaskUtils.enableBackend(backend.getBackendID());
+          // It is necessary to retrieve the backend structure again
+          // because disabling and enabling it again may have resulted
+          // in a new backend being registered to the server.
+          backend = getServerContext().getBackendConfigManager().getLocalBackendById(backend.getBackendID());
+        }
+        catch (DirectoryException e)
+        {
+          logger.traceException(e);
 
-        logger.error(e.getMessageObject());
+          logger.error(e.getMessageObject());
+          backendLeftDisabled = true;
+        }
+      }
+      // Notified once, after the backend is back, so that a listener can read it again.
+      DirectoryServer.notifyImportEnded(backend, importConfig, successful);
+      if (backendLeftDisabled)
+      {
         return TaskState.STOPPED_BY_ERROR;
       }
-      DirectoryServer.notifyImportEnded(backend, importConfig, true);
     }
 
     // Clean up after the import by closing the import config.
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tasks/RestoreTask.java b/opendj-server-legacy/src/main/java/org/opends/server/tasks/RestoreTask.java
index c75a6e2..8568d35 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tasks/RestoreTask.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tasks/RestoreTask.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.tasks;
 
@@ -262,25 +263,32 @@
     // and to take appropriate actions.
     DirectoryServer.notifyRestoreBeginning(backend, restoreConfig);
 
-    // Disable the backend.
-    if ( !verifyOnly)
-    {
-      try
-      {
-        TaskUtils.disableBackend(backendID);
-      } catch (DirectoryException e)
-      {
-        logger.traceException(e);
-
-        logger.error(e.getMessageObject());
-        return TaskState.STOPPED_BY_ERROR;
-      }
-    }
-
-    // From here we must make sure to re-enable the backend before returning.
+    /*
+     * From here the listeners must be told that the restore is over whichever way this
+     * method returns: a listener which took something offline when it began - a
+     * replication domain disables itself - gets no other chance to put it back.
+     */
+    boolean backendDisabled = false;
     boolean errorsEncountered = false;
     try
     {
+      // Disable the backend. From here the finally below re-enables it before returning.
+      if ( !verifyOnly)
+      {
+        try
+        {
+          TaskUtils.disableBackend(backendID);
+          backendDisabled = true;
+        } catch (DirectoryException e)
+        {
+          logger.traceException(e);
+
+          logger.error(e.getMessageObject());
+          errorsEncountered = true;
+          return TaskState.STOPPED_BY_ERROR;
+        }
+      }
+
       // Acquire an exclusive lock for the backend.
       if (verifyOnly || lockBackend(backend))
       {
@@ -294,13 +302,11 @@
           }
           catch (DirectoryException de)
           {
-            DirectoryServer.notifyRestoreEnded(backend, restoreConfig, false);
             logger.error(ERR_RESTOREDB_ERROR_DURING_BACKUP, backupID, backupDir.getPath(), de.getMessageObject());
             errorsEncountered = true;
           }
           catch (Exception e)
           {
-            DirectoryServer.notifyRestoreEnded(backend, restoreConfig, false);
             logger.error(ERR_RESTOREDB_ERROR_DURING_BACKUP, backupID, backupDir.getPath(), getExceptionMessage(e));
             errorsEncountered = true;
           }
@@ -317,8 +323,8 @@
     }
     finally
     {
-      // Enable the backend.
-      if (! verifyOnly)
+      // Enable the backend, if it was this task which disabled it.
+      if (backendDisabled)
       {
         try
         {
@@ -335,7 +341,8 @@
           errorsEncountered = true;
         }
       }
-      DirectoryServer.notifyRestoreEnded(backend, restoreConfig, true);
+      // Notified once, after the backend is back, so that a listener can read it again.
+      DirectoryServer.notifyRestoreEnded(backend, restoreConfig, !errorsEncountered);
     }
 
     if (errorsEncountered)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java b/opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java
index 00467aa..f54a243 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/tasks/TestImportAndExport.java
@@ -22,9 +22,11 @@
 
 import org.forgerock.opendj.ldap.ResultCode;
 import org.opends.server.TestCaseUtils;
+import org.opends.server.api.LocalBackend;
 import org.opends.server.api.TestTaskListener;
 import org.opends.server.backends.task.TaskState;
 import org.opends.server.core.AddOperation;
+import org.opends.server.core.BackendConfigManager;
 import org.opends.server.core.DirectoryServer;
 import org.opends.server.types.Entry;
 import org.forgerock.opendj.ldap.schema.ObjectClass;
@@ -383,6 +385,83 @@
  }
 
   /**
+   * An import which cannot disable its backend must still tell the import task listeners
+   * that the import is over: a listener which took something offline when the import began
+   * - a replication domain disables itself - has no other chance to put it back.
+   */
+  @Test
+  public void testImportEndsWhenTheBackendCannotBeDisabled() throws Exception
+  {
+    /*
+     * A backend registered at runtime has no entry in cn=config, and disabling a backend
+     * is a modification of that entry, so TaskUtils.disableBackend() cannot do it.
+     */
+    final String backendID = "importTaskUnconfiguredBackend";
+    TestCaseUtils.initializeMemoryBackend(backendID, "dc=unconfigured,dc=com", true);
+    try
+    {
+      int importBeginCount = TestTaskListener.importBeginCount.get();
+      int importEndCount   = TestTaskListener.importEndCount.get();
+
+      Entry taskEntry = TestCaseUtils.makeEntry(
+          "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
+          "objectclass: top",
+          "objectclass: ds-task",
+          "objectclass: ds-task-import",
+          "ds-task-class-name: org.opends.server.tasks.ImportTask",
+          "ds-task-import-backend-id: " + backendID,
+          "ds-task-import-ldif-file: " + ldifFile.getPath());
+
+      testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60);
+
+      assertEquals(TestTaskListener.importBeginCount.get(), importBeginCount + 1);
+      assertEquals(TestTaskListener.importEndCount.get(), importEndCount + 1);
+    }
+    finally
+    {
+      removeMemoryBackend(backendID);
+    }
+  }
+
+  /**
+   * A failed import must notify the listeners exactly once, as its beginning was notified
+   * once: a replication domain enabled a second time reloads and rewinds its replication
+   * state for nothing.
+   */
+  @Test
+  public void testFailedImportEndsOnlyOnce() throws Exception
+  {
+    int importBeginCount = TestTaskListener.importBeginCount.get();
+    int importEndCount   = TestTaskListener.importEndCount.get();
+
+    // A directory can be read, so the task accepts it, but it cannot be read as LDIF.
+    Entry taskEntry = TestCaseUtils.makeEntry(
+        "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks",
+        "objectclass: top",
+        "objectclass: ds-task",
+        "objectclass: ds-task-import",
+        "ds-task-class-name: org.opends.server.tasks.ImportTask",
+        "ds-task-import-backend-id: userRoot",
+        "ds-task-import-ldif-file: " + ldifFile.getParent());
+
+    testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60);
+
+    assertEquals(TestTaskListener.importBeginCount.get(), importBeginCount + 1);
+    assertEquals(TestTaskListener.importEndCount.get(), importEndCount + 1);
+  }
+
+  private void removeMemoryBackend(String backendID) throws Exception
+  {
+    BackendConfigManager backendConfigManager = TestCaseUtils.getServerContext().getBackendConfigManager();
+    LocalBackend<?> backend = backendConfigManager.getLocalBackendById(backendID);
+    if (backend != null)
+    {
+      backend.finalizeBackend();
+      backendConfigManager.deregisterLocalBackend(backend);
+    }
+  }
+
+  /**
    * Add a task definition and check that it completes with the expected state.
    * @param taskEntry The task entry.
    * @param resultCode The expected result code of the task add.

--
Gitblit v1.10.0