From 8ef902b7a80dfc0aa75ab840de2c04995385c0a1 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 25 Sep 2026 09:20:01 +0000
Subject: [PATCH] [#1068] Apply what a running backend takes of a configuration change, and ask for a restart for what it does not (#1069)

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java |  162 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 153 insertions(+), 9 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java
index 852943b..a70f9a0 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java
@@ -42,6 +42,7 @@
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
@@ -52,6 +53,7 @@
 import org.forgerock.opendj.config.server.ConfigException;
 import org.forgerock.opendj.ldap.ByteSequence;
 import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.util.Reject;
 import org.forgerock.opendj.config.server.ConfigurationChangeListener;
 import org.forgerock.opendj.server.config.server.JEBackendCfg;
@@ -95,6 +97,8 @@
 import com.sleepycat.je.OperationStatus;
 import com.sleepycat.je.Transaction;
 import com.sleepycat.je.TransactionConfig;
+import com.sleepycat.je.config.ConfigParam;
+import com.sleepycat.je.config.EnvironmentParams;
 
 /** Berkeley DB Java Edition (JE for short) database implementation of the {@link Storage} engine. */
 public final class JEStorage implements Storage, Backupable, ConfigurationChangeListener<JEBackendCfg>,
@@ -721,6 +725,12 @@
 
   private final ServerContext serverContext;
   private final File backendDirectory;
+  /**
+   * The mode last written to the directory the storage runs on. A mode changed along with a move of
+   * db-directory is written to the directory moved to alone, so the configuration as last changed
+   * does not say what the running directory has.
+   */
+  private String runningDirectoryPermissions;
   private JEBackendCfg config;
   private AccessMode accessMode;
 
@@ -791,6 +801,7 @@
     this.maxRetries = maxRetries;
     this.retryWindowNanos = retryWindowNanos;
     backendDirectory = getBackendDirectory(cfg);
+    runningDirectoryPermissions = cfg.getDBDirectoryPermissions();
     config = cfg;
     cfg.addJEChangeListener(this);
   }
@@ -988,6 +999,7 @@
   private void open0() throws ConfigException
   {
     setupStorageFiles(backendDirectory, config.getDBDirectoryPermissions(), config.dn());
+    runningDirectoryPermissions = config.getDBDirectoryPermissions();
     try
     {
       env = new Environment(backendDirectory, envConfig);
@@ -1209,7 +1221,9 @@
   @Override
   public File getDirectory()
   {
-    return getBackendDirectory(config);
+    // The directory the storage runs on: a db-directory moved while it runs is used from the next open,
+    // which a new storage makes.
+    return backendDirectory;
   }
 
   private static File getBackendDirectory(JEBackendCfg cfg)
@@ -1470,7 +1484,8 @@
     final MemoryQuota quota = serverContext.getMemoryQuota();
     return (newSize <= Math.max(reservedCacheSize, computeSize(config))
             || quota.isMemoryAvailable(newSize - reservedCacheSize))
-        && checkConfigurationDirectories(newCfg, unacceptableReasons);
+        && checkConfigurationDirectories(newCfg, unacceptableReasons)
+        && checkEnvironmentConfiguration(newCfg, unacceptableReasons);
   }
 
   private long computeSize(JEBackendCfg cfg)
@@ -1506,7 +1521,28 @@
         return false;
       }
     }
-    return checkConfigurationDirectories(cfg, unacceptableReasons);
+    return checkConfigurationDirectories(cfg, unacceptableReasons)
+        && checkEnvironmentConfiguration(cfg, unacceptableReasons);
+  }
+
+  /**
+   * Whether an environment can be configured from the given configuration. A durability which
+   * sets both flags, or a native property JE does not know, is refused here, before the change
+   * is written - rather than by the next open of the backend, which is where a configuration
+   * nothing checked used to fail.
+   */
+  private static boolean checkEnvironmentConfiguration(JEBackendCfg cfg, List<LocalizableMessage> unacceptableReasons)
+  {
+    try
+    {
+      ConfigurableEnvironment.toEnvironmentConfig(cfg);
+      return true;
+    }
+    catch (ConfigException e)
+    {
+      unacceptableReasons.add(e.getMessageObject());
+      return false;
+    }
   }
 
   private static boolean checkConfigurationDirectories(JEBackendCfg cfg,
@@ -1533,9 +1569,12 @@
     try
     {
       File newBackendDirectory = getBackendDirectory(cfg);
+      // Against the directory the storage runs on rather than the configuration as last changed, so
+      // that a later change still asks for the restart a move is waiting for.
+      final boolean moved = !newBackendDirectory.equals(backendDirectory);
 
       // Create the directory if it doesn't exist.
-      if (!cfg.getDBDirectory().equals(config.getDBDirectory()))
+      if (moved)
       {
         checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, false);
         if (!ccr.getMessages().isEmpty())
@@ -1544,23 +1583,29 @@
         }
 
         ccr.setAdminActionRequired(true);
-        ccr.addMessage(NOTE_CONFIG_DB_DIR_REQUIRES_RESTART.get(config.getDBDirectory(), cfg.getDBDirectory()));
+        ccr.addMessage(NOTE_CONFIG_DB_DIR_REQUIRES_RESTART.get(backendDirectory, newBackendDirectory));
       }
 
-      if (!cfg.getDBDirectoryPermissions().equalsIgnoreCase(config.getDBDirectoryPermissions())
-          || !cfg.getDBDirectory().equals(config.getDBDirectory()))
+      if (!cfg.getDBDirectoryPermissions().equalsIgnoreCase(runningDirectoryPermissions)
+          || moved)
       {
         checkDBDirPermissions(cfg.getDBDirectoryPermissions(), cfg.dn(), ccr);
-        if (!ccr.getMessages().isEmpty())
+        // By its result code: the note of a moved directory is in the result already, and the rest of
+        // the change is still applied and reported alongside it.
+        if (ccr.getResultCode() != ResultCode.SUCCESS)
         {
           return ccr;
         }
 
         setDBDirPermissions(newBackendDirectory, cfg.getDBDirectoryPermissions(), cfg.dn(), ccr);
-        if (!ccr.getMessages().isEmpty())
+        if (ccr.getResultCode() != ResultCode.SUCCESS)
         {
           return ccr;
         }
+        if (!moved)
+        {
+          runningDirectoryPermissions = cfg.getDBDirectoryPermissions();
+        }
       }
       final long newCacheSize = computeSize(cfg);
       if (env != null && newCacheSize != configuredCacheSize)
@@ -1572,6 +1617,12 @@
         ccr.addMessage(
             NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get(cfg.getBackendId(), configuredCacheSize, newCacheSize));
       }
+      // An import runs the environment on a configuration of its own, which goes with it: the backend
+      // opens again on the configuration as changed once the import is over.
+      if (env != null && envConfig.getTransactional())
+      {
+        applyToEnvironment(cfg, ccr);
+      }
       registerMonitoredDirectory(cfg);
       config = cfg;
     }
@@ -1582,6 +1633,99 @@
     return ccr;
   }
 
+  /**
+   * Applies to the running environment what JE takes while it runs, and asks for a restart for what
+   * it takes at the open alone. The environment is configured when it opens, from the configuration
+   * as it is then: a change of a property JE accepts as mutable is handed to the environment here,
+   * and a change of one it does not is reported - the change result reaches the error log, where a
+   * change reported as applied while the environment ran on unchanged until its next open did not.
+   * <p>
+   * The cache is the one mutable setting left where the open put it: it is sized with the memory
+   * reserved for it, and a change of its size asks for a restart above, at which the next open
+   * reserves the new size.
+   */
+  private void applyToEnvironment(JEBackendCfg cfg, ConfigChangeResult ccr) throws ConfigException
+  {
+    final EnvironmentConfig next = ConfigurableEnvironment.toEnvironmentConfig(cfg);
+    final EnvironmentConfig running = env.getConfig();
+    for (ConfigParam param : new TreeMap<>(EnvironmentParams.SUPPORTED_PARAMS).values())
+    {
+      // Replication parameters are not set through an environment configuration; a multi-value
+      // parameter is not read as one value. Neither is set by this storage.
+      if (param.isForReplication() || param.isMultiValueParam())
+      {
+        continue;
+      }
+      final String runningValue = running.getConfigParam(param.getName());
+      final String nextValue = next.getConfigParam(param.getName());
+      if (Objects.equals(runningValue, nextValue)
+          || (param.isMutable()
+              && !switchesOffHeapCache(param.getName(), runningValue, nextValue)
+              && (next.isConfigParamSet(param.getName()) || resetsToDefault(next, param.getName(), nextValue))))
+      {
+        continue;
+      }
+      ccr.setAdminActionRequired(true);
+      ccr.addMessage(NOTE_CONFIG_DB_PROPERTY_REQUIRES_RESTART.get(
+          ConfigurableEnvironment.configuredNameOf(param.getName()), cfg.getBackendId(), runningValue, nextValue));
+    }
+    next.setConfigParam(MAX_MEMORY, running.getConfigParam(MAX_MEMORY));
+    next.setConfigParam(MAX_MEMORY_PERCENT, running.getConfigParam(MAX_MEMORY_PERCENT));
+    // The off-heap cache is switched on or off by the next open alone, which the change asks for above.
+    final String offHeapRunning = running.getConfigParam(MAX_OFF_HEAP_MEMORY);
+    if (switchesOffHeapCache(MAX_OFF_HEAP_MEMORY, offHeapRunning, next.getConfigParam(MAX_OFF_HEAP_MEMORY)))
+    {
+      next.setConfigParam(MAX_OFF_HEAP_MEMORY, offHeapRunning);
+    }
+    // What JE takes while it runs, of the properties the configuration sets; the rest it ignores.
+    env.setMutableConfig(next);
+  }
+
+  /**
+   * Tells whether a change of the given parameter switches JE's off-heap cache on or off. JE takes a
+   * change of the off-heap cache size while it runs, but not one between zero and non-zero: it throws
+   * once it has already taken the new value as its own, and so throws again on every change which
+   * follows, until the next open - which does it.
+   *
+   * @param name the name of the parameter
+   * @param runningValue the value the environment runs with
+   * @param nextValue the value configured
+   * @return whether the change switches the off-heap cache on or off
+   */
+  private static boolean switchesOffHeapCache(String name, String runningValue, String nextValue)
+  {
+    return MAX_OFF_HEAP_MEMORY.equals(name)
+        && (Long.parseLong(runningValue) > 0) != (Long.parseLong(nextValue) > 0);
+  }
+
+  /**
+   * Sets a mutable parameter the configuration no longer sets - a je-property removed - to JE's
+   * default, since the environment keeps the value it runs with of every parameter it is not handed.
+   * A default JE does not take as a value, such as the 0 of je.cleaner.readSize, which JE reads as
+   * "computed at the open", leaves the parameter to the next open.
+   *
+   * @param next the environment configuration handed to the running environment
+   * @param name the name of the parameter
+   * @param defaultValue JE's default of the parameter, as the configuration reads it
+   * @return whether the default is handed to the environment along with the rest
+   */
+  private static boolean resetsToDefault(EnvironmentConfig next, String name, String defaultValue)
+  {
+    if (defaultValue == null)
+    {
+      return false;
+    }
+    try
+    {
+      next.setConfigParam(name, defaultValue);
+      return true;
+    }
+    catch (IllegalArgumentException e)
+    {
+      return false;
+    }
+  }
+
   private void registerMonitoredDirectory(JEBackendCfg cfg)
   {
     diskMonitor.registerMonitoredDirectory(

--
Gitblit v1.10.0