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