From 724371370bdbff2b869530f1fe87fc69364212d7 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 23 Sep 2026 12:38:42 +0000
Subject: [PATCH] [#1067] Ask the memory quota whether an explicit db-cache-size fits instead of taking it (#1070)

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java     |    7 +
 opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/ConfigurableEnvironmentTest.java |  125 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 130 insertions(+), 2 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java
index ab669e8..c364a19 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java
@@ -403,11 +403,14 @@
         throw new ConfigException(ERR_CONFIG_JEB_CACHE_SIZE_TOO_SMALL.get(
             dbCacheSize, MemoryBudget.MIN_MAX_MEMORY_SIZE));
       }
+      // Asked, not taken: the storage this configuration is built for reserves the size itself and
+      // gives that reservation back when it closes. A size the quota cannot grant is only warned
+      // about - the open goes ahead without a reservation, and at startup nothing checks it earlier.
       MemoryQuota memoryQuota = DirectoryServer.getInstance().getServerContext().getMemoryQuota();
-      if (!memoryQuota.acquireMemory(dbCacheSize))
+      if (!memoryQuota.isMemoryAvailable(dbCacheSize))
       {
         logger.warn(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
-            dbCacheSize, memoryQuota.getMaxMemory()));
+            dbCacheSize, memoryQuota.getAvailableMemory()));
       }
     }
   }
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/ConfigurableEnvironmentTest.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/ConfigurableEnvironmentTest.java
new file mode 100644
index 0000000..bd86d1c
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/ConfigurableEnvironmentTest.java
@@ -0,0 +1,125 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.backends.jeb;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
+import static org.mockito.Mockito.when;
+import static org.opends.messages.BackendMessages.ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP;
+import static org.opends.server.util.StaticUtils.MB;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.server.config.server.JEBackendCfg;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.MemoryQuota;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Tests what {@link ConfigurableEnvironment#parseConfigEntry} does to the server's memory quota: an
+ * explicit cache size is checked against it and nothing is taken - the storage which asked for the
+ * configuration reserves the size itself, and gives back what it reserved when it closes.
+ */
+@SuppressWarnings("javadoc")
+public class ConfigurableEnvironmentTest extends DirectoryServerTestCase
+{
+  private static final String BACKEND_ID = "ConfigurableEnvironmentTest";
+  /** A cache size the quota of the test JVM grants several times over, in bytes. */
+  private static final long CACHE_SIZE = 64L * MB;
+
+  @BeforeClass
+  public static void startServer() throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+  /**
+   * The quota the parse checks the size against: the server's own, reached through DirectoryServer
+   * and not through a ServerContext a test could hand it - so the parse is watched on that one.
+   */
+  private static MemoryQuota serverQuota()
+  {
+    return DirectoryServer.getInstance().getServerContext().getMemoryQuota();
+  }
+
+  @Test
+  public void anExplicitCacheSizeTakesNothingFromTheQuota() throws Exception
+  {
+    final long availableBefore = serverQuota().getAvailableMemory();
+
+    ConfigurableEnvironment.parseConfigEntry(createBackendCfg(CACHE_SIZE));
+
+    assertThat(serverQuota().getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  @Test
+  public void aCachePercentTakesNothingFromTheQuota() throws Exception
+  {
+    final long availableBefore = serverQuota().getAvailableMemory();
+
+    ConfigurableEnvironment.parseConfigEntry(createBackendCfg(0L));
+
+    assertThat(serverQuota().getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  /**
+   * A size the quota cannot grant is warned about, naming what the quota has left, and still takes
+   * nothing: the storage's own reservation is what is refused, and the warning is the only word of
+   * it - the open of a backend at startup is not checked against the quota beforehand.
+   */
+  @Test
+  public void aCacheSizeTheQuotaCannotGrantIsWarnedAboutWithWhatIsLeft() throws Exception
+  {
+    final MemoryQuota quota = serverQuota();
+    // all but half a cache size, so that the size does not fit
+    final long held = quota.getAvailableMemory() - CACHE_SIZE / 2;
+    assertThat(quota.acquireMemory(held)).isTrue();
+    try
+    {
+      final long availableBefore = quota.getAvailableMemory();
+      final LocalizableMessage warning =
+          ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(CACHE_SIZE, availableBefore);
+      TestCaseUtils.ERROR_TEXT_WRITER.clear();
+
+      ConfigurableEnvironment.parseConfigEntry(createBackendCfg(CACHE_SIZE));
+
+      assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+      assertThat(TestCaseUtils.ERROR_TEXT_WRITER.getMessages())
+          .as("the size the quota cannot grant is warned about, with what the quota has left")
+          .anyMatch(record -> record.contains("msgID=" + warning.ordinal() + " msg=" + warning));
+    }
+    finally
+    {
+      quota.releaseMemory(held);
+    }
+  }
+
+  /** A configuration whose cache is the given size in bytes, or a fifth of the quota when it is zero. */
+  private static JEBackendCfg createBackendCfg(long cacheSize)
+  {
+    final JEBackendCfg backendCfg = mockCfg(JEBackendCfg.class);
+    when(backendCfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config"));
+    when(backendCfg.getBackendId()).thenReturn(BACKEND_ID);
+    when(backendCfg.getDBCacheSize()).thenReturn(cacheSize);
+    when(backendCfg.getDBCachePercent()).thenReturn(20);
+    when(backendCfg.getDBNumCleanerThreads()).thenReturn(2);
+    when(backendCfg.getDBNumLockTables()).thenReturn(63);
+    return backendCfg;
+  }
+}

--
Gitblit v1.10.0