From 1aa253d7f6f530b9c738ebaf1baf42471dcf01db Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 24 Sep 2026 12:45:23 +0000
Subject: [PATCH] [#1063] Give back what the open reserved rather than what the configuration says by then, and ask for a restart when the cache size changes (#1066)

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java |   62 +++++++++++++++++++++----------
 1 files changed, 42 insertions(+), 20 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 3a0c76e..852943b 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
@@ -728,6 +728,17 @@
   private Environment env;
   private EnvironmentConfig envConfig;
   private MemoryQuota memQuota;
+  /**
+   * The cache size of the configuration this storage opened with, in bytes - what the memory quota
+   * was asked for - and of it, what the quota granted, which is what {@link #close()} gives back.
+   * Both are zero while the storage is closed. Neither is read from {@link #config} again: a
+   * configuration change replaces that while the environment and the reservation stay as the open
+   * made them, so a release computed from it would give back a size that was never taken. For a
+   * cache sized by db-cache-percent this is the quota's count, a percent of its reservable pool, and
+   * not the cache JE runs: JE takes that percent of the maximum heap.
+   */
+  private long configuredCacheSize;
+  private long reservedCacheSize;
   private JEMonitor monitor;
   private DiskSpaceMonitor diskMonitor;
   private StorageStatus storageStatus = StorageStatus.working();
@@ -827,14 +838,10 @@
 
     diskMonitor = serverContext.getDiskSpaceMonitor();
     memQuota = serverContext.getMemoryQuota();
-    if (config.getDBCacheSize() > 0)
-    {
-      memQuota.acquireMemory(config.getDBCacheSize());
-    }
-    else
-    {
-      memQuota.acquireMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
-    }
+    configuredCacheSize = computeSize(config);
+    // A reservation the quota refuses - its budget spent by the other backends, which an open at
+    // startup is not checked against - is nothing to give back: the open goes ahead without it.
+    reservedCacheSize = memQuota.acquireMemory(configuredCacheSize) ? configuredCacheSize : 0;
   }
 
   private DatabaseConfig dbConfig()
@@ -881,14 +888,11 @@
       // another backend be admitted while this one's cache is still resident.
       if (memQuota != null)
       {
-        if (config.getDBCacheSize() > 0)
-        {
-          memQuota.releaseMemory(config.getDBCacheSize());
-        }
-        else
-        {
-          memQuota.releaseMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
-        }
+        // What the open reserved, not what the configuration says by now: a cache size changed
+        // while the storage was open is applied by the next open, which reserves it then.
+        memQuota.releaseMemory(reservedCacheSize);
+        reservedCacheSize = 0;
+        configuredCacheSize = 0;
         // Released once: what an open takes, the next open takes again, and a close which follows
         // a close - BackendImpl.importLDIF closes the storage of its root container however the
         // import ended, on top of the close the import itself made - releases nothing more.
@@ -1457,15 +1461,23 @@
   public boolean isConfigurationChangeAcceptable(JEBackendCfg newCfg,
       List<LocalizableMessage> unacceptableReasons)
   {
-    long newSize = computeSize(newCfg);
-    long oldSize = computeSize(config);
-    return (newSize <= oldSize || memQuota.isMemoryAvailable(newSize - oldSize))
+    // A size which does not grow past the one configured asks the quota for nothing, as before: every
+    // change of the backend entry comes here, the disable of an online import included, and after an
+    // open the quota refused this storage holds nothing to measure such a change against. A growth is
+    // measured against what this storage holds, which is what the next open adds to - not against
+    // config, which a change admitted but not yet applied has already moved to the new size.
+    final long newSize = computeSize(newCfg);
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    return (newSize <= Math.max(reservedCacheSize, computeSize(config))
+            || quota.isMemoryAvailable(newSize - reservedCacheSize))
         && checkConfigurationDirectories(newCfg, unacceptableReasons);
   }
 
   private long computeSize(JEBackendCfg cfg)
   {
-    return cfg.getDBCacheSize() > 0 ? cfg.getDBCacheSize() : memQuota.memPercentToBytes(cfg.getDBCachePercent());
+    return cfg.getDBCacheSize() > 0
+        ? cfg.getDBCacheSize()
+        : serverContext.getMemoryQuota().memPercentToBytes(cfg.getDBCachePercent());
   }
 
   /**
@@ -1550,6 +1562,16 @@
           return ccr;
         }
       }
+      final long newCacheSize = computeSize(cfg);
+      if (env != null && newCacheSize != configuredCacheSize)
+      {
+        // The cache is sized when the environment opens and this storage never resizes it: the next
+        // open of the backend builds it to the new size and reserves that, and until then the
+        // reservation stays with the cache it was made for.
+        ccr.setAdminActionRequired(true);
+        ccr.addMessage(
+            NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get(cfg.getBackendId(), configuredCacheSize, newCacheSize));
+      }
       registerMonitoredDirectory(cfg);
       config = cfg;
     }

--
Gitblit v1.10.0