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