From 159a3b6052447293bc649ac19918bae746466ee7 Mon Sep 17 00:00:00 2001
From: Fabio Pistolesi <fabio.pistolesi@forgerock.com>
Date: Fri, 20 Mar 2015 15:06:51 +0000
Subject: [PATCH] OPENDJ-1727 CR-6350 db-cache-percent default values in persistit backend probably too aggressive

---
 opendj-sdk/opendj-server-legacy/src/messages/org/opends/messages/backend.properties                       |   18 ++-
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PitBackend.java        |    9 +
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/util/ServerConstants.java                 |    5 
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java |    8 +
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java                 |    9 +
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/api/Backend.java                          |    2 
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java  |   58 ++++++++++
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/BackendConfigManager.java            |    9 +
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/ServerContext.java                   |    6 +
 opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/MemoryQuota.java                     |  169 +++++++++++++++++++++++++++++++++
 10 files changed, 279 insertions(+), 14 deletions(-)

diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/api/Backend.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/api/Backend.java
index aacbbf7..d4f59b5 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/api/Backend.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/api/Backend.java
@@ -114,7 +114,7 @@
 
   /**
    * Temporarily sets up the server context for the first phase of add of a new configuration entry.
-   * Will be needed for checking storage parameters before committing the change in configuration.
+   * Needed for checking storage parameters before committing the change in configuration.
    *
    * @param context the server context for this instance
    */
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java
index 4a77501..73e7f55 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/jeb/ConfigurableEnvironment.java
@@ -41,6 +41,8 @@
 import org.opends.server.admin.std.meta.LocalDBBackendCfgDefn;
 import org.opends.server.admin.std.server.LocalDBBackendCfg;
 import org.opends.server.config.ConfigConstants;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.MemoryQuota;
 import org.forgerock.opendj.config.server.ConfigException;
 import com.sleepycat.je.Durability;
 import com.sleepycat.je.EnvironmentConfig;
@@ -470,6 +472,12 @@
             ERR_CONFIG_JEB_CACHE_SIZE_TOO_SMALL.get(
                 cfg.getDBCacheSize(), MemoryBudget.MIN_MAX_MEMORY_SIZE));
       }
+      MemoryQuota memoryQuota = DirectoryServer.getInstance().getServerContext().getMemoryQuota();
+      if (!memoryQuota.acquireMemory(cfg.getDBCacheSize()))
+      {
+        logger.warn(ERR_CONFIG_JEB_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
+            cfg.getDBCacheSize(), memoryQuota.getMaxMemory()));
+      }
     }
 
     EnvironmentConfig envConfig = defaultConfig();
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java
index 086e0d1..92ada67 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java
@@ -30,6 +30,7 @@
 
 import static org.opends.messages.ConfigMessages.*;
 import static org.opends.messages.JebMessages.*;
+import static org.opends.messages.BackendMessages.*;
 import static org.opends.server.util.ServerConstants.*;
 import static org.opends.server.util.StaticUtils.*;
 
@@ -62,6 +63,7 @@
 import org.opends.server.backends.pluggable.spi.WriteOperation;
 import org.opends.server.backends.pluggable.spi.WriteableStorage;
 import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.MemoryQuota;
 import org.opends.server.core.ServerContext;
 import org.opends.server.extensions.DiskSpaceMonitor;
 import org.opends.server.types.DN;
@@ -519,6 +521,7 @@
   private Configuration dbCfg;
   private PersistitBackendCfg config;
   private DiskSpaceMonitor diskMonitor;
+  private MemoryQuota memQuota;
 
   /**
    * Creates a new persistit storage with the provided configuration.
@@ -541,13 +544,17 @@
         BUFFER_SIZE, 4096, Long.MAX_VALUE / BUFFER_SIZE, 2048, true, false, false)));
     final BufferPoolConfiguration bufferPoolCfg = getBufferPoolCfg();
     bufferPoolCfg.setMaximumCount(Integer.MAX_VALUE);
+
+    memQuota = serverContext.getMemoryQuota();
     if (cfg.getDBCacheSize() > 0)
     {
       bufferPoolCfg.setMaximumMemory(cfg.getDBCacheSize());
+      memQuota.acquireMemory(cfg.getDBCacheSize());
     }
     else
     {
       bufferPoolCfg.setFraction(cfg.getDBCachePercent() / 100.0f);
+      memQuota.acquireMemory(memQuota.memPercentToBytes(cfg.getDBCachePercent()));
     }
     dbCfg.setCommitPolicy(cfg.isDBTxnNoSync() ? SOFT : GROUP);
     cfg.addPersistitChangeListener(this);
@@ -569,6 +576,14 @@
         throw new IllegalStateException(e);
       }
     }
+    if (config.getDBCacheSize() > 0)
+    {
+      memQuota.releaseMemory(config.getDBCacheSize());
+    }
+    else
+    {
+      memQuota.releaseMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
+    }
     config.removePersistitChangeListener(this);
     DirectoryServer.deregisterMonitorProvider(diskMonitor);
     DirectoryServer.deregisterAlertGenerator(this);
@@ -792,6 +807,41 @@
   @Override
   public boolean isConfigurationChangeAcceptable(PersistitBackendCfg cfg, List<LocalizableMessage> unacceptableReasons)
   {
+    return checkConfigurationDirectories(cfg, unacceptableReasons);
+  }
+
+  /**
+   * Checks newly created backend has a valid configuration.
+   * @param cfg the new configuration
+   * @param unacceptableReasons the list of accumulated errors and their messages
+   * @param context TODO
+   * @return true if newly created backend has a valid configuration
+   */
+  public static boolean isConfigurationAcceptable(PersistitBackendCfg cfg, List<LocalizableMessage> unacceptableReasons,
+      ServerContext context)
+  {
+    if (context != null)
+    {
+      MemoryQuota memQuota = context.getMemoryQuota();
+      if (cfg.getDBCacheSize() > 0 && !memQuota.isMemoryAvailable(cfg.getDBCacheSize()))
+      {
+        unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
+            cfg.getDBCacheSize(), memQuota.getAvailableMemory()));
+        return false;
+      }
+      else if (!memQuota.isMemoryAvailable(memQuota.memPercentToBytes(cfg.getDBCachePercent())))
+      {
+        unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_PERCENT_GREATER_THAN_JVM_HEAP.get(
+            cfg.getDBCachePercent(), memQuota.memBytesToPercent(memQuota.getAvailableMemory())));
+        return false;
+      }
+    }
+    return checkConfigurationDirectories(cfg, unacceptableReasons);
+  }
+
+  private static boolean checkConfigurationDirectories(PersistitBackendCfg cfg,
+    List<LocalizableMessage> unacceptableReasons)
+  {
     final ConfigChangeResult ccr = new ConfigChangeResult();
     File parentDirectory = getFileForPath(cfg.getDBDirectory());
     File newBackendDirectory = new File(parentDirectory, cfg.getBackendId());
@@ -813,7 +863,7 @@
    * @param ccr the list of reasons to return upstream or null if called from setupStorage()
    * @param cleanup true if the directory should be deleted after creation
    */
-  private void checkDBDirExistsOrCanCreate(File backendDir, ConfigChangeResult ccr, boolean cleanup)
+  private static void checkDBDirExistsOrCanCreate(File backendDir, ConfigChangeResult ccr, boolean cleanup)
   {
     if (!backendDir.exists())
     {
@@ -840,7 +890,7 @@
    * @param ccr the current list of change results
    * @throws forwards a file exception
    */
-  private void checkDBDirPermissions(PersistitBackendCfg cfg, ConfigChangeResult ccr)
+  private static void checkDBDirPermissions(PersistitBackendCfg cfg, ConfigChangeResult ccr)
   {
     try
     {
@@ -887,7 +937,7 @@
     }
   }
 
-  private FilePermission decodeDBDirPermissions(PersistitBackendCfg curCfg) throws ConfigException
+  private static FilePermission decodeDBDirPermissions(PersistitBackendCfg curCfg) throws ConfigException
   {
     try
     {
@@ -947,7 +997,7 @@
     return ccr;
   }
 
-  private void addErrorMessage(final ConfigChangeResult ccr, LocalizableMessage message)
+  private static void addErrorMessage(final ConfigChangeResult ccr, LocalizableMessage message)
   {
     ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
     ccr.addMessage(message);
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PitBackend.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PitBackend.java
index 78a5449..ef4acc8 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PitBackend.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PitBackend.java
@@ -26,6 +26,9 @@
 
 package org.opends.server.backends.persistit;
 
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.opendj.config.server.ConfigException;
 import org.opends.server.admin.std.server.PersistitBackendCfg;
 import org.opends.server.backends.pluggable.BackendImpl;
@@ -37,6 +40,12 @@
 public final class PitBackend extends BackendImpl<PersistitBackendCfg>
 {
   @Override
+  public boolean isConfigurationAcceptable(PersistitBackendCfg cfg, List<LocalizableMessage> unacceptableReasons)
+  {
+    return PersistItStorage.isConfigurationAcceptable(cfg, unacceptableReasons, serverContext);
+  }
+
+  @Override
   protected Storage configureStorage(PersistitBackendCfg cfg) throws ConfigException
   {
     return new PersistItStorage(cfg, serverContext);
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/BackendConfigManager.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/BackendConfigManager.java
index ab1ce73..733777b 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/BackendConfigManager.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/BackendConfigManager.java
@@ -680,10 +680,10 @@
     // backend implementation, then log an error and skip it.
     String className = configEntry.getJavaClass();
 
-    Backend<?> backend;
+    Backend<BackendCfg> backend;
     try
     {
-      backend = loadBackendClass(className).newInstance();
+      backend = (Backend<BackendCfg>)loadBackendClass(className).newInstance();
     }
     catch (Exception e)
     {
@@ -715,6 +715,11 @@
       }
     }
 
+    backend.setServerContext(serverContext);
+    if (!backend.isConfigurationAcceptable(configEntry, unacceptableReason))
+    {
+      return false;
+    }
 
     // If we've gotten to this point, then it is acceptable as far as we are
     // concerned.  If it is unacceptable according to the configuration for that
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
index 8e4506d..3e8e2ae 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
@@ -794,6 +794,9 @@
   /** The writability mode for the Directory Server. */
   private WritabilityMode writabilityMode;
 
+  /** The memory reservation system */
+  private MemoryQuota memoryQuota;
+
   /**
    * The maximum size that internal buffers will be allowed to grow to until
    * they are trimmed.
@@ -890,6 +893,11 @@
       return serverManagementContext;
     }
 
+    @Override
+    public MemoryQuota getMemoryQuota()
+    {
+      return directoryServer.memoryQuota;
+    }
   }
 
 
@@ -921,6 +929,7 @@
     operatingSystem = OperatingSystem.forName(System.getProperty("os.name"));
     serverContext = new DirectoryServerContext();
     virtualAttributeConfigManager = new VirtualAttributeConfigManager(serverContext);
+    memoryQuota = new MemoryQuota();
   }
 
 
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/MemoryQuota.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/MemoryQuota.java
new file mode 100644
index 0000000..a63aa00
--- /dev/null
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/MemoryQuota.java
@@ -0,0 +1,169 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2015 ForgeRock AS.
+ */
+package org.opends.server.core;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryPoolMXBean;
+import java.lang.management.MemoryUsage;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+import static org.opends.server.util.ServerConstants.*;
+
+/**
+ * Estimates the amount of memory in the running JVM for use of long term caches
+ * by looking at the Old Generation, where implemented, or at the Runtime
+ * information as fallback. Allows for reserving memory to avoid over commitment.
+ * There is a fudge factor involved, so it is not byte exact.
+ */
+public final class MemoryQuota
+{
+  private static final double INIT_FUDGE_FACTOR = 0.9;
+  private static final double FUDGE_FACTOR = 1.3;
+  private static final long ONE_MEGABYTE = 1024 * 1024;
+
+  private Semaphore reservedMemory;
+  private int reservableMemory;
+  private boolean allowOvercommit;
+
+  /**
+   * Returns the memory quota reservation system for this server instance.
+   */
+  public MemoryQuota()
+  {
+    allowOvercommit = System.getProperty(ENABLE_MEMORY_OVERCOMMIT) != null;
+    reservableMemory = (int)(INIT_FUDGE_FACTOR * (getOldGenInfo().getMax() / ONE_MEGABYTE));
+    reservedMemory = new Semaphore(reservableMemory, true);
+  }
+
+  /**
+   * Returns the maximum amount of memory the server will use when giving quotas.
+   * @return the maximum amount of memory the server will use when giving quotas
+   */
+  public long getMaxMemory()
+  {
+    return getOldGenInfo().getMax();
+  }
+
+  private MemoryUsage getOldGenInfo()
+  {
+    List<MemoryPoolMXBean> mpools = ManagementFactory.getMemoryPoolMXBeans();
+    for (MemoryPoolMXBean mpool : mpools)
+    {
+      MemoryUsage usage = mpool.getUsage();
+      if (usage != null)
+      {
+        if (mpool.getName().endsWith("Old Gen"))
+        {
+          return usage;
+        }
+      }
+    }
+    Runtime runtime = Runtime.getRuntime();
+    return new MemoryUsage(0, runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory(),
+        runtime.totalMemory(), runtime.maxMemory());
+  }
+
+  /**
+   * Check enough memory is available in the reservable pool.
+   * @param size the amount of requested memory
+   * @return true if enough memory is available in the reservable pool
+  */
+  public boolean isMemoryAvailable(long size)
+  {
+    if (allowOvercommit)
+    {
+      return true;
+    }
+    if (acquireMemory(size))
+    {
+      releaseMemory(size);
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Reserves the requested amount of memory in OldGen.
+   *
+   * @param size the requested amount of memory in bytes
+   * @return true if the requested amount of memory in OldGen could be reserved
+   */
+  public boolean acquireMemory(long size)
+  {
+    if (allowOvercommit)
+    {
+      return true;
+    }
+    return reservedMemory.tryAcquire((int)(FUDGE_FACTOR * size / ONE_MEGABYTE));
+  }
+
+  /**
+   * Returns how much memory is currently not reserved (free) in OldGen.
+   * @return how much memory is currently not reserved (free) in OldGen
+   */
+  public long getAvailableMemory()
+  {
+    if (allowOvercommit)
+    {
+      return reservableMemory * ONE_MEGABYTE;
+    }
+    return reservedMemory.availablePermits() * ONE_MEGABYTE;
+  }
+
+  /**
+   * Translates bytes to percent of reservable memory.
+   * @param size the amount of memory in bytes
+   * @return percent of reservable memory
+   */
+  public int memBytesToPercent(long size)
+  {
+    return (int)(((size / ONE_MEGABYTE) * 100) / reservableMemory);
+  }
+
+  /**
+   * Translates a percentage of memory to the equivalent number of bytes.
+   * @param percent a percentage of memory
+   * @return the equivalent number of bytes
+   */
+  public long memPercentToBytes(int percent)
+  {
+    return (reservableMemory * percent / 100) * ONE_MEGABYTE;
+  }
+
+  /**
+   * Declares OldGen memory is not needed anymore.
+   * @param size the amount of memory to return
+   */
+  public void releaseMemory(long size)
+  {
+    if (allowOvercommit)
+    {
+      return;
+    }
+    reservedMemory.release((int)(FUDGE_FACTOR * size / ONE_MEGABYTE));
+  }
+}
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/ServerContext.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/ServerContext.java
index 956bdf2..5756c7a 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/ServerContext.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/core/ServerContext.java
@@ -80,4 +80,10 @@
    */
   ServerManagementContext getServerManagementContext();
 
+  /**
+   * Returns the memory quota system for reserving long term memory.
+   *
+   * @return the memory quota system
+   */
+  MemoryQuota getMemoryQuota();
 }
diff --git a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/util/ServerConstants.java b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/util/ServerConstants.java
index dca524c..74a51cd 100644
--- a/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/util/ServerConstants.java
+++ b/opendj-sdk/opendj-server-legacy/src/main/java/org/opends/server/util/ServerConstants.java
@@ -3152,6 +3152,9 @@
    */
   public static final String DN_EXTERNAL_CHANGELOG_ROOT = "cn=changelog";
 
-
+  /**
+   * Enable overcommit of memory in Old Gen space.
+   */
+  public static final String ENABLE_MEMORY_OVERCOMMIT = "org.forgerock.opendj.EnableMemoryOvercommit";
 }
 
diff --git a/opendj-sdk/opendj-server-legacy/src/messages/org/opends/messages/backend.properties b/opendj-sdk/opendj-server-legacy/src/messages/org/opends/messages/backend.properties
index 2aecb51..b13ad43 100644
--- a/opendj-sdk/opendj-server-legacy/src/messages/org/opends/messages/backend.properties
+++ b/opendj-sdk/opendj-server-legacy/src/messages/org/opends/messages/backend.properties
@@ -20,7 +20,7 @@
 # CDDL HEADER END
 #
 #      Copyright 2006-2010 Sun Microsystems, Inc.
-#      Portions Copyright 2011-2014 ForgeRock AS
+#      Portions Copyright 2011-2015 ForgeRock AS
 
 
 #
@@ -836,10 +836,6 @@
 ERR_LDIF_BACKEND_ERROR_WRITING_FILE_346=An error occurred while \
  trying to write updated data to file %s for the LDIF backend defined in \
  configuration entry %s:  %s
-ERR_LDIF_BACKEND_ERROR_CLOSING_FILE_426=An error occurred while trying to \
- close file %s for the LDIF backend defined in configuration entry %s:  %s
-ERR_LDIF_BACKEND_ERROR_EMPTY_FILE_427=The file %s written for the LDIF backend \
- defined in configuration entry %s is 0 bytes long and unusable.
 ERR_LDIF_BACKEND_ERROR_RENAMING_FILE_347=An error occurred while \
  attempting to rename file %s to %s while writing updated data for the LDIF \
  backend defined in configuration entry %s:  %s
@@ -1019,4 +1015,14 @@
 ERR_RECURRINGTASK_INVALID_WEEKDAY_TOKEN_SIMPLE_424=The provided \
  recurring task schedule value has an invalid day of the week token
 ERR_SCHEMA_INVALID_REPLACE_MODIFICATION_425=The schema backend does \
- not support the Replace modification type for the %s attribute type
\ No newline at end of file
+ not support the Replace modification type for the %s attribute type
+ERR_LDIF_BACKEND_ERROR_CLOSING_FILE_426=An error occurred while trying to \
+ close file %s for the LDIF backend defined in configuration entry %s:  %s
+ERR_LDIF_BACKEND_ERROR_EMPTY_FILE_427=The file %s written for the LDIF backend \
+ defined in configuration entry %s is 0 bytes long and unusable.
+ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP_428=Configuration \
+ attribute ds-cfg-db-cache-size has a value of %d but the JVM has only \
+ %d available. Consider using ds-cfg-db-cache-percent
+ERR_BACKEND_CONFIG_CACHE_PERCENT_GREATER_THAN_JVM_HEAP_429=Configuration \
+ attribute ds-cfg-db-cache-percent has a value of %d%% but the JVM has only \
+ %d%% available
\ No newline at end of file

--
Gitblit v1.10.0