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-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PDBBackendConfiguration.xml |    7 
 opendj-server-legacy/src/messages/org/opends/messages/backend.properties                                         |    3 
 opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java                                |   64 +++-
 opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/JEStorage.java                                 |   62 +++-
 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JEBackendConfiguration.xml  |    7 
 opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEStorageTest.java                             |  303 +++++++++++++++++++++++
 opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBStorageTest.java                            |  303 +++++++++++++++++++++++
 7 files changed, 703 insertions(+), 46 deletions(-)

diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JEBackendConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JEBackendConfiguration.xml
index c54aa7a..3c539d4 100644
--- a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JEBackendConfiguration.xml
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JEBackendConfiguration.xml
@@ -14,6 +14,7 @@
 
   Copyright 2007-2010 Sun Microsystems, Inc.
   Portions Copyright 2010-2015 ForgeRock AS.
+  Portions Copyright 2026 3A Systems, LLC.
   ! -->
 <adm:managed-object name="je-backend" plural-name="je-backends"
   package="org.forgerock.opendj.server.config"
@@ -148,6 +149,9 @@
       "0 MB". Otherwise, the value of that property is used instead
       to control the cache size configuration.
     </adm:description>
+    <adm:requires-admin-action>
+      <adm:component-restart />
+    </adm:requires-admin-action>
     <adm:default-behavior>
       <adm:defined>
         <adm:value>50</adm:value>
@@ -172,6 +176,9 @@
       db-cache-percent property should be used instead to specify the
       cache size.
     </adm:description>
+    <adm:requires-admin-action>
+      <adm:component-restart />
+    </adm:requires-admin-action>
     <adm:default-behavior>
       <adm:defined>
         <adm:value>0 MB</adm:value>
diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PDBBackendConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PDBBackendConfiguration.xml
index b94b20c..123d781 100644
--- a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PDBBackendConfiguration.xml
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PDBBackendConfiguration.xml
@@ -13,6 +13,7 @@
   information: "Portions Copyright [year] [name of copyright owner]".
 
   Copyright 2014-2015 ForgeRock AS.
+  Portions Copyright 2026 3A Systems, LLC.
   ! -->
 <adm:managed-object name="pdb-backend" plural-name="pdb-backends"
   package="org.forgerock.opendj.server.config"
@@ -122,6 +123,9 @@
       "0 MB". Otherwise, the value of that property is used instead
       to control the cache size configuration.
     </adm:description>
+    <adm:requires-admin-action>
+      <adm:component-restart />
+    </adm:requires-admin-action>
     <adm:default-behavior>
       <adm:defined>
         <adm:value>50</adm:value>
@@ -146,6 +150,9 @@
       db-cache-percent property should be used instead to specify the
       cache size.
     </adm:description>
+    <adm:requires-admin-action>
+      <adm:component-restart />
+    </adm:requires-admin-action>
     <adm:default-behavior>
       <adm:defined>
         <adm:value>0 MB</adm:value>
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;
     }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java
index 3ae64699..916e5a0 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java
@@ -1009,6 +1009,16 @@
   private DiskSpaceMonitor diskMonitor;
   private PDBMonitor monitor;
   private MemoryQuota memQuota;
+  /**
+   * The cache size of the configuration this storage opened with, in bytes - what the buffer pool was
+   * built to and 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 pool and the reservation
+   * stay as the open made them, so a release computed from it would give back a size that was never
+   * taken.
+   */
+  private long configuredCacheSize;
+  private long reservedCacheSize;
   private StorageStatus storageStatus = StorageStatus.working();
   /** Attempt bound of a {@link WriteableStorageImpl#write}, {@link #MAX_RETRIES} outside the tests. */
   private final int maxRetries;
@@ -1084,16 +1094,11 @@
 
     diskMonitor = serverContext.getDiskSpaceMonitor();
     memQuota = serverContext.getMemoryQuota();
-    if (config.getDBCacheSize() > 0)
-    {
-      bufferPoolCfg.setMaximumMemory(config.getDBCacheSize());
-      memQuota.acquireMemory(config.getDBCacheSize());
-    }
-    else
-    {
-      bufferPoolCfg.setMaximumMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
-      memQuota.acquireMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
-    }
+    configuredCacheSize = computeSize(config);
+    bufferPoolCfg.setMaximumMemory(configuredCacheSize);
+    // 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;
     commitPolicy = config.isDBTxnNoSync() ? SOFT : GROUP;
     dbCfg.setJmxEnabled(false);
     return dbCfg;
@@ -1127,14 +1132,11 @@
       // 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.
@@ -1547,15 +1549,23 @@
   public boolean isConfigurationChangeAcceptable(PDBBackendCfg 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(PDBBackendCfg cfg)
   {
-    return cfg.getDBCacheSize() > 0 ? cfg.getDBCacheSize() : memQuota.memPercentToBytes(cfg.getDBCachePercent());
+    return cfg.getDBCacheSize() > 0
+        ? cfg.getDBCacheSize()
+        : serverContext.getMemoryQuota().memPercentToBytes(cfg.getDBCachePercent());
   }
 
   /**
@@ -1640,6 +1650,16 @@
           return ccr;
         }
       }
+      final long newCacheSize = computeSize(cfg);
+      if (db != null && newCacheSize != configuredCacheSize)
+      {
+        // The buffer pool is sized when the database opens and PersistIt has no way to resize it: the
+        // next open of the backend builds it to the new size and reserves that, and until then the
+        // reservation stays with the pool it was made for.
+        ccr.setAdminActionRequired(true);
+        ccr.addMessage(
+            NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get(cfg.getBackendId(), configuredCacheSize, newCacheSize));
+      }
       registerMonitoredDirectory(cfg);
       config = cfg;
       commitPolicy = config.isDBTxnNoSync() ? SOFT : GROUP;
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/backend.properties b/opendj-server-legacy/src/messages/org/opends/messages/backend.properties
index f4aa843..58d6a7b 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/backend.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/backend.properties
@@ -1170,3 +1170,6 @@
 ERR_CONFIG_INDEX_ATTRIBUTE_ALREADY_INDEXED_629=Attribute %s of backend base DN '%s' is already indexed by %s. \
  An attribute type is indexed once, whichever of its names or its OID the index is declared by, so change that \
  index instead of adding another one for the same attribute
+NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART_630=The change to the database cache of backend %s will not take effect \
+ until the backend is restarted: until then the cache keeps the size the backend was opened with, which the \
+ memory quota counts as %d bytes, and the next open reserves the %d bytes the quota counts for the new configuration
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEStorageTest.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEStorageTest.java
index a3494c3d..6150a5a 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEStorageTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEStorageTest.java
@@ -23,18 +23,25 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.opends.messages.BackendMessages.NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART;
 import static org.opends.server.util.CollectionUtils.newTreeSet;
+import static org.opends.server.util.StaticUtils.MB;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.config.server.ConfigChangeResult;
 import org.forgerock.opendj.config.server.ConfigException;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.server.config.server.JEBackendCfg;
 import org.opends.server.DirectoryServerTestCase;
 import org.opends.server.TestCaseUtils;
@@ -89,6 +96,8 @@
   private static final String LOCK_TIMEOUT_LONGER_THAN_SHORT_WINDOW = "300 ms";
   /** How long a test waits for a thread it started, in seconds; well past any bound the tests configure. */
   private static final long WAIT_SECONDS = 60;
+  /** A cache size the quota of the test JVM grants several times over, in bytes. */
+  private static final long SMALL_CACHE = 64L * MB;
 
   private final TreeName treeName = new TreeName("dc=test", "test");
   private ServerContext serverContext;
@@ -245,6 +254,284 @@
     assertThat(read("missing")).isNull();
   }
 
+  /**
+   * A cache size changed while the storage is open is given back as it was taken: the close
+   * releases what the open reserved, not what the configuration says by then. Read from the
+   * configuration at both ends, a change in between drifts the quota by the difference for the
+   * life of the JVM - the open which follows reserves the new size and pays nothing back.
+   */
+  @Test
+  public void aCacheGrownWhileOpenIsGivenBackAsItWasTaken() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore - SMALL_CACHE);
+
+    storage.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+    storage.close();
+
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  /** The shrink is the same drift the other way: the difference stays reserved by nobody. */
+  @Test
+  public void aCacheShrunkWhileOpenIsGivenBackAsItWasTaken() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new JEStorage(createBackendCfg(2 * SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(createBackendCfg(SMALL_CACHE));
+    // A shrink asks for the restart as a growth does: the cache keeps the size it was opened with.
+    assertThat(ccr.adminActionRequired()).isTrue();
+    assertThat(ccr.getMessages()).hasSize(1);
+    assertThat(ccr.getMessages().get(0).toString()).isEqualTo(
+        NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get(BACKEND_ID, 2 * SMALL_CACHE, SMALL_CACHE).toString());
+    storage.close();
+
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  /**
+   * The cache is sized when the environment opens and this storage never resizes it, so a change
+   * of the cache size is applied by the next open of the backend - and the operator is told so,
+   * rather than that the change applied.
+   */
+  @Test
+  public void aCacheSizeChangedWhileOpenAsksForARestart() throws Exception
+  {
+    closeAndRemove(storage);
+    storage = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+
+    assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    assertThat(ccr.adminActionRequired()).isTrue();
+    assertThat(ccr.getMessages()).hasSize(1);
+    assertThat(ccr.getMessages().get(0).ordinal()).isEqualTo(NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.ordinal());
+    assertThat(ccr.getMessages().get(0).toString()).isEqualTo(
+        NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get(BACKEND_ID, SMALL_CACHE, 2 * SMALL_CACHE).toString());
+
+    // The cache still runs at the size it was opened with, whatever the change before said: back to
+    // that size, there is nothing left to restart for.
+    final ConfigChangeResult back = storage.applyConfigurationChange(createBackendCfg(SMALL_CACHE));
+    assertThat(back.adminActionRequired()).isFalse();
+    assertThat(back.getMessages()).isEmpty();
+  }
+
+  /**
+   * The default cache is sized by db-cache-percent, db-cache-size left at 0: the restart is asked
+   * for by the size the percentage comes to, not by db-cache-size, which does not move.
+   */
+  @Test
+  public void aCacheSizedByPercentAsksForARestartOnlyWhenThePercentChanges() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new JEStorage(createBackendCfg(0L, 10), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    final JEBackendCfg unchangedCache = createBackendCfg(0L, 10);
+    when(unchangedCache.isDBTxnNoSync()).thenReturn(true);
+
+    final ConfigChangeResult unchanged = storage.applyConfigurationChange(unchangedCache);
+    assertThat(unchanged.adminActionRequired()).isFalse();
+    assertThat(unchanged.getMessages()).isEmpty();
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(createBackendCfg(0L, 20));
+    assertThat(ccr.adminActionRequired()).isTrue();
+    assertThat(ccr.getMessages()).hasSize(1);
+    assertThat(ccr.getMessages().get(0).toString()).isEqualTo(NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get(
+        BACKEND_ID, quota.memPercentToBytes(10), quota.memPercentToBytes(20)).toString());
+  }
+
+  /**
+   * A storage which has not opened runs no cache to restart, and a change of the cache size asks it
+   * for none. The listener is registered by the constructor already.
+   */
+  @Test
+  public void aStorageWhichIsNotOpenAsksForNoRestart() throws Exception
+  {
+    final JEStorage unopened = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    try
+    {
+      final ConfigChangeResult ccr = unopened.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+
+      assertThat(ccr.adminActionRequired()).isFalse();
+      for (LocalizableMessage message : ccr.getMessages())
+      {
+        assertThat(message.ordinal()).isNotEqualTo(NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.ordinal());
+      }
+    }
+    finally
+    {
+      unopened.close();
+    }
+  }
+
+  /** A change which leaves the cache size alone asks for nothing, as before. */
+  @Test
+  public void aChangeWhichLeavesTheCacheSizeAloneAsksForNothing() throws Exception
+  {
+    closeAndRemove(storage);
+    storage = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    final JEBackendCfg unchangedCache = createBackendCfg(SMALL_CACHE);
+    when(unchangedCache.isDBTxnNoSync()).thenReturn(true);
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(unchangedCache);
+
+    assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    assertThat(ccr.adminActionRequired()).isFalse();
+    assertThat(ccr.getMessages()).isEmpty();
+  }
+
+  /**
+   * A change of the cache size is admitted against what the storage holds of the quota, which is
+   * what the next open has to add to. Once a change has been admitted but not applied, the
+   * configuration says the new size while the reservation is still the old one, and a check
+   * against the configuration would admit a second change the server has no memory for.
+   */
+  @Test
+  public void aCacheSizeChangeIsAdmittedAgainstWhatTheStorageHolds() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    storage.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+    // Room for two caches and a bit: the difference to the configured size, not to the reserved one.
+    assertThat(quota.acquireMemory(quota.getAvailableMemory() - 2 * SMALL_CACHE - MB)).isTrue();
+
+    final List<LocalizableMessage> reasons = new ArrayList<>();
+    assertThat(storage.isConfigurationChangeAcceptable(createBackendCfg(4 * SMALL_CACHE), reasons))
+        .as("four caches, with one reserved and two and a bit free").isFalse();
+    assertThat(storage.isConfigurationChangeAcceptable(createBackendCfg(3 * SMALL_CACHE), reasons))
+        .as("three caches, with one reserved and two and a bit free").isTrue();
+  }
+
+  /**
+   * A reservation the quota refused is not given back on close. The open goes ahead without it -
+   * the quota is a budget, not a lock - but a close which released what was never taken would
+   * hand the quota memory the server does not have.
+   */
+  @Test
+  public void aReservationTheQuotaRefusedIsNotGivenBackOnClose() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    // Half a cache left in the quota: the reservation of a whole one is refused.
+    assertThat(quota.acquireMemory(quota.getAvailableMemory() - SMALL_CACHE / 2)).isTrue();
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+
+    storage.close();
+
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  /**
+   * After an open the quota refused, the storage holds nothing of the quota, and a change which
+   * leaves the cache size alone - any other property, the disable an online import makes - still
+   * asks the quota for nothing: every change of the backend entry is put to this storage.
+   */
+  @Test
+  public void aChangeWhichLeavesTheCacheSizeAloneIsAdmittedAfterARefusedReservation() throws Exception
+  {
+    openWithTheReservationRefused();
+    final JEBackendCfg unchangedCache = createBackendCfg(SMALL_CACHE);
+    when(unchangedCache.isDBTxnNoSync()).thenReturn(true);
+
+    assertThat(storage.isConfigurationChangeAcceptable(unchangedCache, new ArrayList<LocalizableMessage>()))
+        .isTrue();
+  }
+
+  /**
+   * A growth after an open the quota refused is measured against what the storage holds, which is
+   * nothing: a quarter of a cache more than configured is a cache and a quarter more than held.
+   */
+  @Test
+  public void aGrowthAfterARefusedReservationIsMeasuredAgainstNothingHeld() throws Exception
+  {
+    openWithTheReservationRefused();
+
+    assertThat(storage.isConfigurationChangeAcceptable(
+        createBackendCfg(SMALL_CACHE + SMALL_CACHE / 4), new ArrayList<LocalizableMessage>()))
+        .as("a cache and a quarter, with nothing held and half a cache free").isFalse();
+  }
+
+  /** A shrink asks the quota for nothing, even with none of it left. */
+  @Test
+  public void aShrinkIsAdmittedWithTheQuotaExhausted() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.acquireMemory(quota.getAvailableMemory())).isTrue();
+
+    assertThat(storage.isConfigurationChangeAcceptable(
+        createBackendCfg(SMALL_CACHE / 2), new ArrayList<LocalizableMessage>())).isTrue();
+  }
+
+  /**
+   * After a shrink while open, the storage still holds the cache it was opened with, and a growth
+   * back within that asks the quota for nothing, even with none of it left: measured against the
+   * configuration alone, it would ask the quota for the negative difference to what is held.
+   */
+  @Test
+  public void aGrowthWithinWhatIsHeldAfterAShrinkAsksTheQuotaForNothing() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new JEStorage(createBackendCfg(2 * SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    storage.applyConfigurationChange(createBackendCfg(SMALL_CACHE));
+    assertThat(quota.acquireMemory(quota.getAvailableMemory())).isTrue();
+
+    assertThat(storage.isConfigurationChangeAcceptable(
+        createBackendCfg(SMALL_CACHE + SMALL_CACHE / 2), new ArrayList<LocalizableMessage>())).isTrue();
+  }
+
+  /**
+   * A storage which is not open yet - its listener is registered by the constructor, the open comes
+   * later - admits a change of a cache sized by percent: the size is counted by the quota of the
+   * server context, not by the one the open keeps, which is not there yet.
+   */
+  @Test
+  public void aStorageWhichIsNotOpenAdmitsAChangeOfItsCachePercent() throws Exception
+  {
+    final JEStorage unopened = new JEStorage(createBackendCfg(0L, 10), serverContext);
+    try
+    {
+      assertThat(unopened.isConfigurationChangeAcceptable(
+          createBackendCfg(0L, 20), new ArrayList<LocalizableMessage>())).isTrue();
+    }
+    finally
+    {
+      unopened.close();
+    }
+  }
+
+  /** Opens a storage of one cache with half a cache left in the quota, so that its reservation is refused. */
+  private void openWithTheReservationRefused() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    assertThat(quota.acquireMemory(quota.getAvailableMemory() - SMALL_CACHE / 2)).isTrue();
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new JEStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
   /** A storage whose directory is a regular file, which no open of it can use. */
   private JEStorage blockedStorage(JEBackendCfg cfg) throws Exception
   {
@@ -757,13 +1044,25 @@
 
   private static JEBackendCfg createBackendCfg()
   {
+    return createBackendCfg(0L);
+  }
+
+  /** 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)
+  {
+    return createBackendCfg(cacheSize, 20);
+  }
+
+  /** A configuration whose cache is the given size in bytes, or the given percent of the quota when it is zero. */
+  private static JEBackendCfg createBackendCfg(long cacheSize, int cachePercent)
+  {
     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.getDBDirectory()).thenReturn(BACKEND_ID);
     when(backendCfg.getDBDirectoryPermissions()).thenReturn("755");
-    when(backendCfg.getDBCacheSize()).thenReturn(0L);
-    when(backendCfg.getDBCachePercent()).thenReturn(20);
+    when(backendCfg.getDBCacheSize()).thenReturn(cacheSize);
+    when(backendCfg.getDBCachePercent()).thenReturn(cachePercent);
     when(backendCfg.getDBNumCleanerThreads()).thenReturn(2);
     when(backendCfg.getDBNumLockTables()).thenReturn(63);
     return backendCfg;
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBStorageTest.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBStorageTest.java
index c52dd07..49f3929 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBStorageTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBStorageTest.java
@@ -21,12 +21,18 @@
 import static org.forgerock.opendj.config.ConfigurationMock.*;
 import static org.opends.server.util.StaticUtils.*;
 import static org.forgerock.opendj.ldap.ByteString.*;
+import static org.opends.messages.BackendMessages.*;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.config.server.ConfigChangeResult;
 import org.forgerock.opendj.config.server.ConfigException;
 import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
 import org.opends.server.DirectoryServerTestCase;
 import org.opends.server.TestCaseUtils;
 import org.forgerock.opendj.server.config.server.PDBBackendCfg;
@@ -63,6 +69,9 @@
   private ServerContext serverContext;
   private PDBStorage storage;
 
+  /** A cache size the quota of the test JVM grants several times over, in bytes. */
+  private static final long SMALL_CACHE = 64L * MB;
+
   @BeforeClass
   public static void startServer() throws Exception
   {
@@ -550,6 +559,284 @@
     storage.open(AccessMode.READ_WRITE);
   }
 
+  /**
+   * A cache size changed while the storage is open is given back as it was taken: the close
+   * releases what the open reserved, not what the configuration says by then. Read from the
+   * configuration at both ends, a change in between drifts the quota by the difference for the
+   * life of the JVM - the open which follows reserves the new size and pays nothing back.
+   */
+  @Test
+  public void aCacheGrownWhileOpenIsGivenBackAsItWasTaken() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore - SMALL_CACHE);
+
+    storage.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+    storage.close();
+
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  /** The shrink is the same drift the other way: the difference stays reserved by nobody. */
+  @Test
+  public void aCacheShrunkWhileOpenIsGivenBackAsItWasTaken() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new PDBStorage(createBackendCfg(2 * SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(createBackendCfg(SMALL_CACHE));
+    // A shrink asks for the restart as a growth does: the cache keeps the size it was opened with.
+    assertThat(ccr.adminActionRequired()).isTrue();
+    assertThat(ccr.getMessages()).hasSize(1);
+    assertThat(ccr.getMessages().get(0).toString()).isEqualTo(
+        NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get("PDBStorageTest", 2 * SMALL_CACHE, SMALL_CACHE).toString());
+    storage.close();
+
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  /**
+   * The buffer pool is sized when the database opens and PersistIt has no way to resize it, so a
+   * change of the cache size is applied by the next open of the backend - and the operator is told
+   * so, rather than that the change applied.
+   */
+  @Test
+  public void aCacheSizeChangedWhileOpenAsksForARestart() throws Exception
+  {
+    closeAndRemove(storage);
+    storage = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+
+    assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    assertThat(ccr.adminActionRequired()).isTrue();
+    assertThat(ccr.getMessages()).hasSize(1);
+    assertThat(ccr.getMessages().get(0).ordinal()).isEqualTo(NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.ordinal());
+    assertThat(ccr.getMessages().get(0).toString()).isEqualTo(
+        NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get("PDBStorageTest", SMALL_CACHE, 2 * SMALL_CACHE).toString());
+
+    // The pool still runs at the size it was opened with, whatever the change before said: back to
+    // that size, there is nothing left to restart for.
+    final ConfigChangeResult back = storage.applyConfigurationChange(createBackendCfg(SMALL_CACHE));
+    assertThat(back.adminActionRequired()).isFalse();
+    assertThat(back.getMessages()).isEmpty();
+  }
+
+  /**
+   * The default cache is sized by db-cache-percent, db-cache-size left at 0: the restart is asked
+   * for by the size the percentage comes to, not by db-cache-size, which does not move.
+   */
+  @Test
+  public void aCacheSizedByPercentAsksForARestartOnlyWhenThePercentChanges() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new PDBStorage(createBackendCfg(0L, 10), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    final PDBBackendCfg unchangedCache = createBackendCfg(0L, 10);
+    when(unchangedCache.isDBTxnNoSync()).thenReturn(true);
+
+    final ConfigChangeResult unchanged = storage.applyConfigurationChange(unchangedCache);
+    assertThat(unchanged.adminActionRequired()).isFalse();
+    assertThat(unchanged.getMessages()).isEmpty();
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(createBackendCfg(0L, 20));
+    assertThat(ccr.adminActionRequired()).isTrue();
+    assertThat(ccr.getMessages()).hasSize(1);
+    assertThat(ccr.getMessages().get(0).toString()).isEqualTo(NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.get(
+        "PDBStorageTest", quota.memPercentToBytes(10), quota.memPercentToBytes(20)).toString());
+  }
+
+  /**
+   * A storage which has not opened runs no cache to restart, and a change of the cache size asks it
+   * for none. The listener is registered by the constructor already.
+   */
+  @Test
+  public void aStorageWhichIsNotOpenAsksForNoRestart() throws Exception
+  {
+    final PDBStorage unopened = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    try
+    {
+      final ConfigChangeResult ccr = unopened.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+
+      assertThat(ccr.adminActionRequired()).isFalse();
+      for (LocalizableMessage message : ccr.getMessages())
+      {
+        assertThat(message.ordinal()).isNotEqualTo(NOTE_CONFIG_DB_CACHE_REQUIRES_RESTART.ordinal());
+      }
+    }
+    finally
+    {
+      unopened.close();
+    }
+  }
+
+  /** A change which leaves the cache size alone asks for nothing, as before. */
+  @Test
+  public void aChangeWhichLeavesTheCacheSizeAloneAsksForNothing() throws Exception
+  {
+    closeAndRemove(storage);
+    storage = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    final PDBBackendCfg unchangedCache = createBackendCfg(SMALL_CACHE);
+    when(unchangedCache.isDBTxnNoSync()).thenReturn(true);
+
+    final ConfigChangeResult ccr = storage.applyConfigurationChange(unchangedCache);
+
+    assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    assertThat(ccr.adminActionRequired()).isFalse();
+    assertThat(ccr.getMessages()).isEmpty();
+  }
+
+  /**
+   * A change of the cache size is admitted against what the storage holds of the quota, which is
+   * what the next open has to add to. Once a change has been admitted but not applied, the
+   * configuration says the new size while the reservation is still the old one, and a check
+   * against the configuration would admit a second change the server has no memory for.
+   */
+  @Test
+  public void aCacheSizeChangeIsAdmittedAgainstWhatTheStorageHolds() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    storage.applyConfigurationChange(createBackendCfg(2 * SMALL_CACHE));
+    // Room for two caches and a bit: the difference to the configured size, not to the reserved one.
+    assertThat(quota.acquireMemory(quota.getAvailableMemory() - 2 * SMALL_CACHE - MB)).isTrue();
+
+    final List<LocalizableMessage> reasons = new ArrayList<>();
+    assertThat(storage.isConfigurationChangeAcceptable(createBackendCfg(4 * SMALL_CACHE), reasons))
+        .as("four caches, with one reserved and two and a bit free").isFalse();
+    assertThat(storage.isConfigurationChangeAcceptable(createBackendCfg(3 * SMALL_CACHE), reasons))
+        .as("three caches, with one reserved and two and a bit free").isTrue();
+  }
+
+  /**
+   * A reservation the quota refused is not given back on close. The open goes ahead without it -
+   * the quota is a budget, not a lock - but a close which released what was never taken would
+   * hand the quota memory the server does not have.
+   */
+  @Test
+  public void aReservationTheQuotaRefusedIsNotGivenBackOnClose() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    // Half a cache left in the quota: the reservation of a whole one is refused.
+    assertThat(quota.acquireMemory(quota.getAvailableMemory() - SMALL_CACHE / 2)).isTrue();
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+
+    storage.close();
+
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
+  /**
+   * After an open the quota refused, the storage holds nothing of the quota, and a change which
+   * leaves the cache size alone - any other property, the disable an online import makes - still
+   * asks the quota for nothing: every change of the backend entry is put to this storage.
+   */
+  @Test
+  public void aChangeWhichLeavesTheCacheSizeAloneIsAdmittedAfterARefusedReservation() throws Exception
+  {
+    openWithTheReservationRefused();
+    final PDBBackendCfg unchangedCache = createBackendCfg(SMALL_CACHE);
+    when(unchangedCache.isDBTxnNoSync()).thenReturn(true);
+
+    assertThat(storage.isConfigurationChangeAcceptable(unchangedCache, new ArrayList<LocalizableMessage>()))
+        .isTrue();
+  }
+
+  /**
+   * A growth after an open the quota refused is measured against what the storage holds, which is
+   * nothing: a quarter of a cache more than configured is a cache and a quarter more than held.
+   */
+  @Test
+  public void aGrowthAfterARefusedReservationIsMeasuredAgainstNothingHeld() throws Exception
+  {
+    openWithTheReservationRefused();
+
+    assertThat(storage.isConfigurationChangeAcceptable(
+        createBackendCfg(SMALL_CACHE + SMALL_CACHE / 4), new ArrayList<LocalizableMessage>()))
+        .as("a cache and a quarter, with nothing held and half a cache free").isFalse();
+  }
+
+  /** A shrink asks the quota for nothing, even with none of it left. */
+  @Test
+  public void aShrinkIsAdmittedWithTheQuotaExhausted() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.acquireMemory(quota.getAvailableMemory())).isTrue();
+
+    assertThat(storage.isConfigurationChangeAcceptable(
+        createBackendCfg(SMALL_CACHE / 2), new ArrayList<LocalizableMessage>())).isTrue();
+  }
+
+  /**
+   * After a shrink while open, the storage still holds the cache it was opened with, and a growth
+   * back within that asks the quota for nothing, even with none of it left: measured against the
+   * configuration alone, it would ask the quota for the negative difference to what is held.
+   */
+  @Test
+  public void aGrowthWithinWhatIsHeldAfterAShrinkAsksTheQuotaForNothing() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    storage = new PDBStorage(createBackendCfg(2 * SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    storage.applyConfigurationChange(createBackendCfg(SMALL_CACHE));
+    assertThat(quota.acquireMemory(quota.getAvailableMemory())).isTrue();
+
+    assertThat(storage.isConfigurationChangeAcceptable(
+        createBackendCfg(SMALL_CACHE + SMALL_CACHE / 2), new ArrayList<LocalizableMessage>())).isTrue();
+  }
+
+  /**
+   * A storage which is not open yet - its listener is registered by the constructor, the open comes
+   * later - admits a change of a cache sized by percent: the size is counted by the quota of the
+   * server context, not by the one the open keeps, which is not there yet.
+   */
+  @Test
+  public void aStorageWhichIsNotOpenAdmitsAChangeOfItsCachePercent() throws Exception
+  {
+    final PDBStorage unopened = new PDBStorage(createBackendCfg(0L, 10), serverContext);
+    try
+    {
+      assertThat(unopened.isConfigurationChangeAcceptable(
+          createBackendCfg(0L, 20), new ArrayList<LocalizableMessage>())).isTrue();
+    }
+    finally
+    {
+      unopened.close();
+    }
+  }
+
+  /** Opens a storage of one cache with half a cache left in the quota, so that its reservation is refused. */
+  private void openWithTheReservationRefused() throws Exception
+  {
+    final MemoryQuota quota = serverContext.getMemoryQuota();
+    closeAndRemove(storage);
+    assertThat(quota.acquireMemory(quota.getAvailableMemory() - SMALL_CACHE / 2)).isTrue();
+    final long availableBefore = quota.getAvailableMemory();
+    storage = new PDBStorage(createBackendCfg(SMALL_CACHE), serverContext);
+    storage.open(AccessMode.READ_WRITE);
+    assertThat(quota.getAvailableMemory()).isEqualTo(availableBefore);
+  }
+
   private void createTree() throws Exception
   {
     storage.write(new WriteOperation()
@@ -576,12 +863,24 @@
 
   protected PDBBackendCfg createBackendCfg()
   {
+    return createBackendCfg(0L);
+  }
+
+  /** A configuration whose cache is the given size in bytes, or a fifth of the quota when it is zero. */
+  private static PDBBackendCfg createBackendCfg(long cacheSize)
+  {
+    return createBackendCfg(cacheSize, 20);
+  }
+
+  /** A configuration whose cache is the given size in bytes, or the given percent of the quota when it is zero. */
+  private static PDBBackendCfg createBackendCfg(long cacheSize, int cachePercent)
+  {
     PDBBackendCfg backendCfg = mockCfg(PDBBackendCfg.class);
     when(backendCfg.getBackendId()).thenReturn("PDBStorageTest");
     when(backendCfg.getDBDirectory()).thenReturn("PDBStorageTest");
     when(backendCfg.getDBDirectoryPermissions()).thenReturn("755");
-    when(backendCfg.getDBCacheSize()).thenReturn(0L);
-    when(backendCfg.getDBCachePercent()).thenReturn(20);
+    when(backendCfg.getDBCacheSize()).thenReturn(cacheSize);
+    when(backendCfg.getDBCachePercent()).thenReturn(cachePercent);
     return backendCfg;
   }
 

--
Gitblit v1.10.0