001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2010-2016 ForgeRock AS.
016 */
017package org.opends.server.backends.jeb;
018
019import static com.sleepycat.je.EnvironmentConfig.*;
020
021import static org.opends.messages.BackendMessages.*;
022import static org.opends.messages.ConfigMessages.*;
023
024import java.lang.reflect.Method;
025import java.math.BigInteger;
026import java.util.Arrays;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.SortedSet;
032import java.util.StringTokenizer;
033import java.util.concurrent.TimeUnit;
034import java.util.logging.Level;
035import java.util.logging.Logger;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.i18n.slf4j.LocalizedLogger;
039import org.forgerock.opendj.config.server.ConfigException;
040import org.forgerock.opendj.ldap.ByteString;
041import org.forgerock.opendj.ldap.DN;
042import org.opends.server.admin.BooleanPropertyDefinition;
043import org.opends.server.admin.DurationPropertyDefinition;
044import org.opends.server.admin.PropertyDefinition;
045import org.opends.server.admin.std.meta.JEBackendCfgDefn;
046import org.opends.server.admin.std.server.BackendCfg;
047import org.opends.server.admin.std.server.JEBackendCfg;
048import org.opends.server.config.ConfigConstants;
049import org.opends.server.core.DirectoryServer;
050import org.opends.server.core.MemoryQuota;
051import org.opends.server.util.Platform;
052
053import com.sleepycat.je.Durability;
054import com.sleepycat.je.EnvironmentConfig;
055import com.sleepycat.je.dbi.MemoryBudget;
056
057/** This class maps JE properties to configuration attributes. */
058public class ConfigurableEnvironment
059{
060  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
061
062  /**
063   * The name of the attribute which configures the database cache size as a
064   * percentage of Java VM heap size.
065   */
066  public static final String ATTR_DATABASE_CACHE_PERCENT =
067       ConfigConstants.NAME_PREFIX_CFG + "db-cache-percent";
068
069  /**
070   * The name of the attribute which configures the database cache size as an
071   * approximate number of bytes.
072   */
073  public static final String ATTR_DATABASE_CACHE_SIZE =
074       ConfigConstants.NAME_PREFIX_CFG + "db-cache-size";
075
076  /**
077   * The name of the attribute which configures whether data updated by a
078   * database transaction is forced to disk.
079   */
080  public static final String ATTR_DATABASE_TXN_NO_SYNC =
081       ConfigConstants.NAME_PREFIX_CFG + "db-txn-no-sync";
082
083  /**
084   * The name of the attribute which configures whether data updated by a
085   * database transaction is written from the Java VM to the O/S.
086   */
087  public static final String ATTR_DATABASE_TXN_WRITE_NO_SYNC =
088       ConfigConstants.NAME_PREFIX_CFG + "db-txn-write-no-sync";
089
090  /**
091   * The name of the attribute which configures whether the database background
092   * cleaner thread runs.
093   */
094  public static final String ATTR_DATABASE_RUN_CLEANER =
095       ConfigConstants.NAME_PREFIX_CFG + "db-run-cleaner";
096
097  /**
098   * The name of the attribute which configures the minimum percentage of log
099   * space that must be used in log files.
100   */
101  public static final String ATTR_CLEANER_MIN_UTILIZATION =
102       ConfigConstants.NAME_PREFIX_CFG + "db-cleaner-min-utilization";
103
104  /**
105   * The name of the attribute which configures the maximum size of each
106   * individual JE log file, in bytes.
107   */
108  public static final String ATTR_DATABASE_LOG_FILE_MAX =
109       ConfigConstants.NAME_PREFIX_CFG + "db-log-file-max";
110
111  /** The name of the attribute which configures the database cache eviction algorithm. */
112  public static final String ATTR_EVICTOR_LRU_ONLY =
113       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-lru-only";
114
115  /**
116   * The name of the attribute which configures the number of nodes in one scan
117   * of the database cache evictor.
118   */
119  public static final String ATTR_EVICTOR_NODES_PER_SCAN =
120       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-nodes-per-scan";
121
122  /**
123   * The name of the attribute which configures the minimum number of threads
124   * of the database cache evictor pool.
125   */
126  public static final String ATTR_EVICTOR_CORE_THREADS =
127       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-core-threads";
128  /**
129   * The name of the attribute which configures the maximum number of threads
130   * of the database cache evictor pool.
131   */
132  public static final String ATTR_EVICTOR_MAX_THREADS =
133       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-max-threads";
134
135  /**
136   * The name of the attribute which configures the time excess threads
137   * of the database cache evictor pool are kept alive.
138   */
139  public static final String ATTR_EVICTOR_KEEP_ALIVE =
140       ConfigConstants.NAME_PREFIX_CFG + "db-evictor-keep-alive";
141
142  /**
143   * The name of the attribute which configures whether the logging file
144   * handler will be on or off.
145   */
146  public static final String ATTR_LOGGING_FILE_HANDLER_ON =
147       ConfigConstants.NAME_PREFIX_CFG + "db-logging-file-handler-on";
148
149  /** The name of the attribute which configures the trace logging message level. */
150  public static final String ATTR_LOGGING_LEVEL =
151       ConfigConstants.NAME_PREFIX_CFG + "db-logging-level";
152
153  /**
154   * The name of the attribute which configures how many bytes are written to
155   * the log before the checkpointer runs.
156   */
157  public static final String ATTR_CHECKPOINTER_BYTES_INTERVAL =
158       ConfigConstants.NAME_PREFIX_CFG + "db-checkpointer-bytes-interval";
159
160  /**
161   * The name of the attribute which configures the amount of time between
162   * runs of the checkpointer.
163   */
164  public static final String ATTR_CHECKPOINTER_WAKEUP_INTERVAL =
165       ConfigConstants.NAME_PREFIX_CFG +
166       "db-checkpointer-wakeup-interval";
167
168  /** The name of the attribute which configures the number of lock tables. */
169  public static final String ATTR_NUM_LOCK_TABLES =
170       ConfigConstants.NAME_PREFIX_CFG + "db-num-lock-tables";
171
172  /**
173   * The name of the attribute which configures the number threads
174   * allocated by the cleaner for log file processing.
175   */
176  public static final String ATTR_NUM_CLEANER_THREADS =
177       ConfigConstants.NAME_PREFIX_CFG + "db-num-cleaner-threads";
178
179  /** The name of the attribute which configures the size of the file handle cache. */
180  public static final String ATTR_LOG_FILECACHE_SIZE =
181       ConfigConstants.NAME_PREFIX_CFG + "db-log-filecache-size";
182
183  /** The name of the attribute which may specify any native JE properties. */
184  public static final String ATTR_JE_PROPERTY =
185       ConfigConstants.NAME_PREFIX_CFG + "je-property";
186
187  /** A map of JE property names to the corresponding configuration attribute. */
188  private static HashMap<String, String> attrMap = new HashMap<>();
189
190  /**
191   * A map of configuration attribute names to the corresponding configuration object getter method.
192   */
193  private static Map<String, Method> jebMethodMap = new HashMap<>();
194  /** A map of configuration attribute names to the corresponding configuration PropertyDefinition. */
195  private static Map<String, PropertyDefinition<?>> jebDefnMap = new HashMap<>();
196
197  /** Pulled from resource/admin/ABBREVIATIONS.xsl. db is mose common. */
198  private static final List<String> ABBREVIATIONS = Arrays.asList(new String[]
199          {"aci", "ip", "ssl", "dn", "rdn", "jmx", "smtp", "http",
200           "https", "ldap", "ldaps", "ldif", "jdbc", "tcp", "tls",
201           "pkcs11", "sasl", "gssapi", "md5", "je", "dse", "fifo",
202           "vlv", "uuid", "md5", "sha1", "sha256", "sha384", "sha512",
203           "tls", "db"});
204
205  /** E.g. db-cache-percent -> DBCachePercent */
206  private static String propNametoCamlCase(String hyphenated)
207  {
208    String[] components = hyphenated.split("\\-");
209    StringBuilder buffer = new StringBuilder();
210    for (String component: components) {
211      if (ABBREVIATIONS.contains(component)) {
212        buffer.append(component.toUpperCase());
213      } else {
214        buffer.append(component.substring(0, 1).toUpperCase()).append(component.substring(1));
215      }
216    }
217    return buffer.toString();
218  }
219
220  /**
221   * Register a JE property and its corresponding configuration attribute.
222   *
223   * @param propertyName The name of the JE property to be registered.
224   * @param attrName     The name of the configuration attribute associated
225   *                     with the property.
226   * @throws Exception   If there is an error in the attribute name.
227   */
228  private static void registerProp(String propertyName, String attrName)
229       throws Exception
230  {
231    // Strip off NAME_PREFIX_CFG.
232    String baseName = attrName.substring(7);
233    String methodBaseName = propNametoCamlCase(baseName);
234
235    registerJebProp(attrName, methodBaseName);
236    attrMap.put(propertyName, attrName);
237  }
238
239  private static void registerJebProp(String attrName, String methodBaseName) throws Exception
240  {
241    Class<JEBackendCfg> configClass = JEBackendCfg.class;
242    JEBackendCfgDefn defn = JEBackendCfgDefn.getInstance();
243    Class<? extends JEBackendCfgDefn> defClass = defn.getClass();
244
245    String propName = "get" + methodBaseName + "PropertyDefinition";
246    PropertyDefinition<?> propDefn = (PropertyDefinition<?>) defClass.getMethod(propName).invoke(defn);
247
248    String methodPrefix = propDefn instanceof BooleanPropertyDefinition ? "is" : "get";
249    String methodName = methodPrefix + methodBaseName;
250
251    jebDefnMap.put(attrName, propDefn);
252    jebMethodMap.put(attrName, configClass.getMethod(methodName));
253  }
254
255  /**
256   * Get the name of the configuration attribute associated with a JE property.
257   * @param jeProperty The name of the JE property.
258   * @return The name of the associated configuration attribute.
259   */
260  public static String getAttributeForProperty(String jeProperty)
261  {
262    return attrMap.get(jeProperty);
263  }
264
265  /**
266   * Get the value of a JE property that is mapped to a configuration attribute.
267   * @param cfg The configuration containing the property values.
268   * @param attrName The configuration attribute type name.
269   * @return The string value of the JE property.
270   */
271  private static String getPropertyValue(BackendCfg cfg, String attrName, ByteString backendId)
272  {
273    try
274    {
275      PropertyDefinition<?> propDefn = jebDefnMap.get(attrName);
276      Method method = jebMethodMap.get(attrName);
277
278      if (propDefn instanceof DurationPropertyDefinition)
279      {
280        Long value = (Long)method.invoke(cfg);
281
282        // JE durations are in microseconds so we must convert.
283        DurationPropertyDefinition durationPropDefn =
284             (DurationPropertyDefinition)propDefn;
285        value = 1000*durationPropDefn.getBaseUnit().toMilliSeconds(value);
286
287        return String.valueOf(value);
288      }
289      else
290      {
291        Object value = method.invoke(cfg);
292
293        if (value != null)
294        {
295          return String.valueOf(value);
296        }
297
298        if (attrName.equals(ATTR_NUM_CLEANER_THREADS))
299        {
300          // Automatically choose based on the number of processors. We will use
301          // similar heuristics to those used to define the default number of
302          // worker threads.
303          value = Platform.computeNumberOfThreads(8, 1.0f);
304
305          logger.debug(INFO_ERGONOMIC_SIZING_OF_JE_CLEANER_THREADS,
306              backendId, (Number) value);
307        }
308        else if (attrName.equals(ATTR_NUM_LOCK_TABLES))
309        {
310          // Automatically choose based on the number of processors. We'll assume that the user has also allowed
311          // automatic configuration of cleaners and workers.
312          BigInteger tmp = BigInteger.valueOf(Platform.computeNumberOfThreads(1, 2));
313          value = tmp.nextProbablePrime();
314
315          logger.debug(INFO_ERGONOMIC_SIZING_OF_JE_LOCK_TABLES, backendId, (Number) value);
316        }
317
318        return String.valueOf(value);
319      }
320    }
321    catch (Exception e)
322    {
323      logger.traceException(e);
324      return "";
325    }
326  }
327
328  static
329  {
330    // Register the parameters that have JE property names.
331    try
332    {
333      registerProp("je.maxMemoryPercent", ATTR_DATABASE_CACHE_PERCENT);
334      registerProp("je.maxMemory", ATTR_DATABASE_CACHE_SIZE);
335      registerProp("je.cleaner.minUtilization", ATTR_CLEANER_MIN_UTILIZATION);
336      registerProp("je.env.runCleaner", ATTR_DATABASE_RUN_CLEANER);
337      registerProp("je.evictor.lruOnly", ATTR_EVICTOR_LRU_ONLY);
338      registerProp("je.evictor.nodesPerScan", ATTR_EVICTOR_NODES_PER_SCAN);
339      registerProp("je.evictor.coreThreads", ATTR_EVICTOR_CORE_THREADS);
340      registerProp("je.evictor.maxThreads", ATTR_EVICTOR_MAX_THREADS);
341      registerProp("je.evictor.keepAlive", ATTR_EVICTOR_KEEP_ALIVE);
342      registerProp("je.log.fileMax", ATTR_DATABASE_LOG_FILE_MAX);
343      registerProp("je.checkpointer.bytesInterval",
344                   ATTR_CHECKPOINTER_BYTES_INTERVAL);
345      registerProp("je.checkpointer.wakeupInterval",
346                   ATTR_CHECKPOINTER_WAKEUP_INTERVAL);
347      registerProp("je.lock.nLockTables", ATTR_NUM_LOCK_TABLES);
348      registerProp("je.cleaner.threads", ATTR_NUM_CLEANER_THREADS);
349      registerProp("je.log.fileCacheSize", ATTR_LOG_FILECACHE_SIZE);
350    }
351    catch (Exception e)
352    {
353      logger.traceException(e);
354    }
355  }
356
357  /**
358   * Create a JE environment configuration with default values.
359   *
360   * @return A JE environment config containing default values.
361   */
362  public static EnvironmentConfig defaultConfig()
363  {
364    EnvironmentConfig envConfig = new EnvironmentConfig();
365
366    envConfig.setTransactional(true);
367    envConfig.setAllowCreate(true);
368
369    // "je.env.sharedLatches" is "true" by default since JE #12136 (3.3.62?)
370
371    // This parameter was set to false while diagnosing a Berkeley DB JE bug.
372    // Normally cleansed log files are deleted, but if this is set false
373    // they are instead renamed from .jdb to .del.
374    envConfig.setConfigParam(CLEANER_EXPUNGE, "true");
375
376    // Under heavy write load the check point can fall behind causing
377    // uncontrolled DB growth over time. This parameter makes the out of
378    // the box configuration more robust at the cost of a slight
379    // reduction in maximum write throughput. Experiments have shown
380    // that response time predictability is not impacted negatively.
381    envConfig.setConfigParam(CHECKPOINTER_HIGH_PRIORITY, "true");
382
383    // If the JVM is reasonably large then we can safely default to
384    // bigger read buffers. This will result in more scalable checkpointer
385    // and cleaner performance.
386    if (Runtime.getRuntime().maxMemory() > 256 * 1024 * 1024)
387    {
388      envConfig.setConfigParam(CLEANER_LOOK_AHEAD_CACHE_SIZE,
389          String.valueOf(2 * 1024 * 1024));
390      envConfig.setConfigParam(LOG_ITERATOR_READ_SIZE,
391          String.valueOf(2 * 1024 * 1024));
392      envConfig.setConfigParam(LOG_FAULT_READ_SIZE, String.valueOf(4 * 1024));
393    }
394
395    // Disable lock timeouts, meaning that no lock wait
396    // timelimit is enforced and a deadlocked operation
397    // will block indefinitely.
398    envConfig.setLockTimeout(0, TimeUnit.MICROSECONDS);
399
400    return envConfig;
401  }
402
403  /**
404   * Parse a configuration associated with a JE environment and create an
405   * environment config from it.
406   *
407   * @param cfg The configuration to be parsed.
408   * @return An environment config instance corresponding to the config entry.
409   * @throws ConfigException If there is an error in the provided configuration
410   * entry.
411   */
412  public static EnvironmentConfig parseConfigEntry(JEBackendCfg cfg) throws ConfigException
413  {
414    validateDbCacheSize(cfg.getDBCacheSize());
415
416    EnvironmentConfig envConfig = defaultConfig();
417    setDurability(envConfig, cfg.isDBTxnNoSync(), cfg.isDBTxnWriteNoSync());
418    setJEProperties(cfg, envConfig, cfg.dn().rdn().getFirstAVA().getAttributeValue());
419    setDBLoggingLevel(envConfig, cfg.getDBLoggingLevel(), cfg.dn(), cfg.isDBLoggingFileHandlerOn());
420
421    // See if there are any native JE properties specified in the config
422    // and if so try to parse, evaluate and set them.
423    return setJEProperties(envConfig, cfg.getJEProperty(), attrMap);
424  }
425
426  private static void validateDbCacheSize(long dbCacheSize) throws ConfigException
427  {
428    if (dbCacheSize != 0)
429    {
430      if (MemoryBudget.getRuntimeMaxMemory() < dbCacheSize)
431      {
432        throw new ConfigException(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
433            dbCacheSize, MemoryBudget.getRuntimeMaxMemory()));
434      }
435      if (dbCacheSize < MemoryBudget.MIN_MAX_MEMORY_SIZE)
436      {
437        throw new ConfigException(ERR_CONFIG_JEB_CACHE_SIZE_TOO_SMALL.get(
438            dbCacheSize, MemoryBudget.MIN_MAX_MEMORY_SIZE));
439      }
440      MemoryQuota memoryQuota = DirectoryServer.getInstance().getServerContext().getMemoryQuota();
441      if (!memoryQuota.acquireMemory(dbCacheSize))
442      {
443        logger.warn(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
444            dbCacheSize, memoryQuota.getMaxMemory()));
445      }
446    }
447  }
448
449  private static void setDurability(EnvironmentConfig envConfig, boolean dbTxnNoSync, boolean dbTxnWriteNoSync)
450      throws ConfigException
451  {
452    if (dbTxnNoSync && dbTxnWriteNoSync)
453    {
454      throw new ConfigException(ERR_CONFIG_JEB_DURABILITY_CONFLICT.get());
455    }
456    if (dbTxnNoSync)
457    {
458      envConfig.setDurability(Durability.COMMIT_NO_SYNC);
459    }
460    else if (dbTxnWriteNoSync)
461    {
462      envConfig.setDurability(Durability.COMMIT_WRITE_NO_SYNC);
463    }
464  }
465
466  private static void setJEProperties(BackendCfg cfg, EnvironmentConfig envConfig, ByteString backendId)
467  {
468    for (Map.Entry<String, String> mapEntry : attrMap.entrySet())
469    {
470      String jeProperty = mapEntry.getKey();
471      String attrName = mapEntry.getValue();
472
473      String value = getPropertyValue(cfg, attrName, backendId);
474      envConfig.setConfigParam(jeProperty, value);
475    }
476  }
477
478  private static void setDBLoggingLevel(EnvironmentConfig envConfig, String loggingLevel, DN dn,
479      boolean loggingFileHandlerOn) throws ConfigException
480  {
481    Logger parent = Logger.getLogger("com.sleepycat.je");
482    try
483    {
484      parent.setLevel(Level.parse(loggingLevel));
485    }
486    catch (Exception e)
487    {
488      throw new ConfigException(ERR_JEB_INVALID_LOGGING_LEVEL.get(loggingLevel, dn));
489    }
490
491    final Level level = loggingFileHandlerOn ? Level.ALL : Level.OFF;
492    envConfig.setConfigParam(FILE_LOGGING_LEVEL, level.getName());
493  }
494
495  /**
496   * Parse, validate and set native JE environment properties for
497   * a given environment config.
498   *
499   * @param  envConfig The JE environment config for which to set
500   *                   the properties.
501   * @param  jeProperties The JE environment properties to parse,
502   *                      validate and set.
503   * @param  configAttrMap Component supported JE properties to
504   *                       their configuration attributes map.
505   * @return An environment config instance with given properties
506   *         set.
507   * @throws ConfigException If there is an error while parsing,
508   *         validating and setting any of the properties provided.
509   */
510  public static EnvironmentConfig setJEProperties(EnvironmentConfig envConfig,
511    SortedSet<String> jeProperties, HashMap<String, String> configAttrMap)
512    throws ConfigException
513  {
514    if (jeProperties.isEmpty()) {
515      // return default config.
516      return envConfig;
517    }
518
519    // Set to catch duplicate properties.
520    HashSet<String> uniqueJEProperties = new HashSet<>();
521
522    // Iterate through the config values associated with a JE property.
523    for (String jeEntry : jeProperties)
524    {
525      StringTokenizer st = new StringTokenizer(jeEntry, "=");
526      if (st.countTokens() != 2)
527      {
528        throw new ConfigException(ERR_CONFIG_JE_PROPERTY_INVALID_FORM.get(jeEntry));
529      }
530
531      String jePropertyName = st.nextToken();
532      String jePropertyValue = st.nextToken();
533      // Check if it is a duplicate.
534      if (uniqueJEProperties.contains(jePropertyName)) {
535        throw new ConfigException(ERR_CONFIG_JE_DUPLICATE_PROPERTY.get(jePropertyName));
536      }
537
538      // Set JE property.
539      try {
540        envConfig.setConfigParam(jePropertyName, jePropertyValue);
541        // If this property shadows an existing config attribute.
542        if (configAttrMap.containsKey(jePropertyName)) {
543          LocalizableMessage message = ERR_CONFIG_JE_PROPERTY_SHADOWS_CONFIG.get(
544            jePropertyName, attrMap.get(jePropertyName));
545          throw new ConfigException(message);
546        }
547        // Add this property to unique set.
548        uniqueJEProperties.add(jePropertyName);
549      } catch(IllegalArgumentException e) {
550        logger.traceException(e);
551        LocalizableMessage message = ERR_CONFIG_JE_PROPERTY_INVALID.get(jeEntry, e.getMessage());
552        throw new ConfigException(message, e.getCause());
553      }
554    }
555
556    return envConfig;
557  }
558}