/* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 * * * Portions Copyright 2006-2007 Sun Microsystems, Inc. */ package org.opends.server.backends.jeb; import com.sleepycat.je.config.EnvironmentParams; import com.sleepycat.je.config.ConfigParam; import com.sleepycat.je.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.*; import java.io.File; import java.io.FilenameFilter; import org.opends.server.monitors.DatabaseEnvironmentMonitor; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DN; import org.opends.server.types.ErrorLogCategory; import org.opends.server.types.ErrorLogSeverity; import org.opends.server.types.FilePermission; import org.opends.server.types.ConfigChangeResult; import org.opends.server.types.ResultCode; import static org.opends.server.loggers.Error.logError; import static org.opends.server.loggers.debug.DebugLogger.debugInfo; import static org.opends.server.loggers.debug.DebugLogger.debugCaught; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import static org.opends.server.messages.MessageHandler.getMessage; import static org.opends.server.messages.JebMessages. MSGID_JEB_CACHE_SIZE_AFTER_PRELOAD; import static org.opends.server.messages.JebMessages. MSGID_JEB_CLEAN_DATABASE_START; import static org.opends.server.messages.JebMessages. MSGID_JEB_CLEAN_DATABASE_MARKED; import static org.opends.server.messages.JebMessages. MSGID_JEB_CLEAN_DATABASE_FINISH; import static org.opends.server.messages.JebMessages. MSGID_JEB_SET_PERMISSIONS_FAILED; import static org.opends.server.messages.JebMessages. MSGID_JEB_CONFIG_ATTR_REQUIRES_RESTART; import org.opends.server.api.Backend; import org.opends.server.admin.std.server.JEBackendCfg; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.core.DirectoryServer; /** * Wrapper class for the JE environment. Root container holds all the entry * containers for each base DN. It also maintains all the openings and closings * of the entry containers. */ public class RootContainer implements ConfigurationChangeListener { /** * The JE database environment. */ private Environment env; /** * The backend configuration. */ private Config config; /** * The backend to which this entry root container belongs. */ private Backend backend; /** * The database environment monitor for this JE environment. */ private DatabaseEnvironmentMonitor monitor; /** * The base DNs contained in this entryContainer. */ private ConcurrentHashMap entryContainers; /** * The cached value of the next entry identifier to be assigned. */ private AtomicLong nextid = new AtomicLong(1); /** * Creates a new RootContainer object. Each root container represents a JE * environment. * * @param config The configuration of the JE backend. * @param backend A reference to the JE back end that is creating this * root container. */ public RootContainer(Config config, Backend backend) { this.env = null; this.monitor = null; this.entryContainers = new ConcurrentHashMap(); this.backend = backend; this.config = config; } /** * Helper method to apply database directory permissions and create a new * JE environment. * * @param backendDirectory The environment home directory for JE. * @param backendPermission The file permissions for the environment home * directory. * @param envConfig The JE environment configuration. * @throws DatabaseException If an error occurs when creating the environment. */ private void open(File backendDirectory, FilePermission backendPermission, EnvironmentConfig envConfig) throws DatabaseException { // Get the backend database backendDirectory permissions and apply if(FilePermission.canSetPermissions()) { try { if(!FilePermission.setPermissions(backendDirectory, backendPermission)) { throw new Exception(); } } catch(Exception e) { // Log an warning that the permissions were not set. int msgID = MSGID_JEB_SET_PERMISSIONS_FAILED; String message = getMessage(msgID, backendDirectory.getPath()); logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.SEVERE_WARNING, message, msgID); } } // Open the database environment env = new Environment(backendDirectory, envConfig); if (debugEnabled()) { debugInfo("JE (%s) environment opened with the following config: %n%s", JEVersion.CURRENT_VERSION.toString(), env.getConfig().toString()); // Get current size of heap in bytes long heapSize = Runtime.getRuntime().totalMemory(); // Get maximum size of heap in bytes. The heap cannot grow beyond this size. // Any attempt will result in an OutOfMemoryException. long heapMaxSize = Runtime.getRuntime().maxMemory(); // Get amount of free memory within the heap in bytes. This size will // increase // after garbage collection and decrease as new objects are created. long heapFreeSize = Runtime.getRuntime().freeMemory(); debugInfo("Current size of heap: %d bytes", heapSize); debugInfo("Max size of heap: %d bytes", heapMaxSize); debugInfo("Free memory in heap: %d bytes", heapFreeSize); } } /** * Opens the root container. * * @throws DatabaseException If an error occurs when opening the container. */ public void open() throws DatabaseException { open(config.getBackendDirectory(), config.getBackendPermission(), config.getEnvironmentConfig()); } /** * Opens the root container using the configuration parameters provided. Any * configuration parameters provided will override the parameters in the * JE configuration object. * * @param backendDirectory The environment home directory for JE. * @param backendPermission he file permissions for the environment home * directory. * @param readOnly Open the container in read only mode. * @param allowCreate Allow creating new entries in the container. * @param transactional Use transactions on operations. * @param txnNoSync Use asynchronous transactions. * @param isLocking Create the environment with locking. * @param runCheckPointer Start the checkpointer. * @throws DatabaseException If an error occurs when openinng the container. */ public void open(File backendDirectory, FilePermission backendPermission, boolean readOnly, boolean allowCreate, boolean transactional, boolean txnNoSync, boolean isLocking, boolean runCheckPointer) throws DatabaseException { EnvironmentConfig envConfig; if(config.getEnvironmentConfig() != null) { envConfig = config.getEnvironmentConfig(); } else { envConfig = new EnvironmentConfig(); } envConfig.setReadOnly(readOnly); envConfig.setAllowCreate(allowCreate); envConfig.setTransactional(transactional); envConfig.setTxnNoSync(txnNoSync); envConfig.setConfigParam("je.env.isLocking", String.valueOf(isLocking)); envConfig.setConfigParam("je.env.runCheckpointer", String.valueOf(runCheckPointer)); open(backendDirectory, backendPermission, envConfig); } /** * Opens the entry container for a base DN. If the entry container does not * exist for the base DN, it will be created. The entry container will be * opened with the same mode as the root container. Any entry containers * opened in a read only root container will also be read only. Any entry * containers opened in a non transactional root container will also be non * transactional. * * @param baseDN The base DN of the entry container to open. * @return The opened entry container. * @throws DatabaseException If an error occurs while opening the entry * container. */ public EntryContainer openEntryContainer(DN baseDN) throws DatabaseException { EntryContainer ec = new EntryContainer(baseDN, backend, config, env, this); EntryContainer ec1=this.entryContainers.get(baseDN); //If an entry container for this baseDN is already open we don't allow //another to be opened. if (ec1 != null) throw new DatabaseException("Entry container for baseDN " + baseDN.toString() + " already is open."); if(env.getConfig().getReadOnly()) { ec.openReadOnly(); } else if(!env.getConfig().getTransactional()) { ec.openNonTransactional(true); } else { ec.open(); } this.entryContainers.put(baseDN, ec); return ec; } /** * Opens the entry containers for multiple base DNs. * * @param baseDNs The base DNs of the entry containers to open. * @throws DatabaseException If an error occurs while opening the entry * container. */ public void openEntryContainers(DN[] baseDNs) throws DatabaseException { EntryID id; EntryID highestID = null; for(DN baseDN : baseDNs) { EntryContainer ec = openEntryContainer(baseDN); id = ec.getHighestEntryID(); if(highestID == null || id.compareTo(highestID) > 0) { highestID = id; } } nextid = new AtomicLong(highestID.longValue() + 1); } /** * Close the entry container for a base DN. * * @param baseDN The base DN of the entry container to close. * @throws DatabaseException If an error occurs while closing the entry * container. */ public void closeEntryContainer(DN baseDN) throws DatabaseException { getEntryContainer(baseDN).close(); entryContainers.remove(baseDN); } /** * Close and remove a entry container for a base DN from disk. * * @param baseDN The base DN of the entry container to remove. * @throws DatabaseException If an error occurs while removing the entry * container. */ public void removeEntryContainer(DN baseDN) throws DatabaseException { getEntryContainer(baseDN).close(); getEntryContainer(baseDN).removeContainer(); entryContainers.remove(baseDN); } /** * Get the DatabaseEnvironmentMonitor object for JE environment used by this * root container. * * @return The DatabaseEnvironmentMonito object. */ public DatabaseEnvironmentMonitor getMonitorProvider() { if(monitor == null) { String monitorName = backend.getBackendID() + " Database Environment"; monitor = new DatabaseEnvironmentMonitor(monitorName, env); } return monitor; } /** * Preload the database cache. There is no preload if the configured preload * time limit is zero. */ public void preload() { long timeLimit = config.getPreloadTimeLimit(); if (timeLimit > 0) { // Get a list of all the databases used by the backend. ArrayList dbList = new ArrayList(); for (EntryContainer ec : entryContainers.values()) { ec.listDatabases(dbList); } // Sort the list in order of priority. Collections.sort(dbList, new DbPreloadComparator()); // Preload each database until we reach the time limit or the cache // is filled. try { long timeEnd = System.currentTimeMillis() + timeLimit; // Configure preload of Leaf Nodes (LNs) containing the data values. PreloadConfig preloadConfig = new PreloadConfig(); preloadConfig.setLoadLNs(true); for (Database db : dbList) { // Calculate the remaining time. long timeRemaining = timeEnd - System.currentTimeMillis(); if (timeRemaining <= 0) { break; } preloadConfig.setMaxMillisecs(timeRemaining); PreloadStats preloadStats = db.preload(preloadConfig); if(debugEnabled()) { debugInfo("file=" + db.getDatabaseName() + " LNs=" + preloadStats.getNLNsLoaded()); } // Stop if the cache is full or the time limit has been exceeded. if (preloadStats.getStatus() != PreloadStatus.SUCCESS) { break; } } // Log an informational message about the size of the cache. EnvironmentStats stats = env.getStats(new StatsConfig()); long total = stats.getCacheTotalBytes(); int msgID = MSGID_JEB_CACHE_SIZE_AFTER_PRELOAD; String message = getMessage(msgID, total / (1024 * 1024)); logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.NOTICE, message, msgID); } catch (DatabaseException e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } } } } /** * Synchronously invokes the cleaner on the database environment then forces a * checkpoint to delete the log files that are no longer in use. * * @throws DatabaseException If an error occurs while cleaning the database * environment. */ private void cleanDatabase() throws DatabaseException { int msgID; String message; FilenameFilter filenameFilter = new FilenameFilter() { public boolean accept(File d, String name) { return name.endsWith(".jdb"); } }; File backendDirectory = env.getHome(); int beforeLogfileCount = backendDirectory.list(filenameFilter).length; msgID = MSGID_JEB_CLEAN_DATABASE_START; message = getMessage(msgID, beforeLogfileCount, backendDirectory.getPath()); logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.NOTICE, message, msgID); int currentCleaned = 0; int totalCleaned = 0; while ((currentCleaned = env.cleanLog()) > 0) { totalCleaned += currentCleaned; } msgID = MSGID_JEB_CLEAN_DATABASE_MARKED; message = getMessage(msgID, totalCleaned); logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.NOTICE, message, msgID); if (totalCleaned > 0) { CheckpointConfig force = new CheckpointConfig(); force.setForce(true); env.checkpoint(force); } int afterLogfileCount = backendDirectory.list(filenameFilter).length; msgID = MSGID_JEB_CLEAN_DATABASE_FINISH; message = getMessage(msgID, afterLogfileCount); logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.NOTICE, message, msgID); } /** * Close the root entryContainer. * * @throws DatabaseException If an error occurs while attempting to close * the entryContainer. */ public void close() throws DatabaseException { for(DN baseDN : entryContainers.keySet()) { entryContainers.get(baseDN).close(); entryContainers.remove(baseDN); } env.close(); } /** * Return all the entry containers in this root container. * * @return The entry containers in this root container. */ public Collection getEntryContainers() { return entryContainers.values(); } /** * Returns all the baseDNs this root container stores. * * @return The set of DNs this root container stores. */ public Set getBaseDNs() { return entryContainers.keySet(); } /** * Return the entry container for a specific base DN. * * @param baseDN The base DN of the entry container to retrive. * @return The entry container for the base DN. */ public EntryContainer getEntryContainer(DN baseDN) { EntryContainer ec = null; DN nodeDN = baseDN; while (ec == null && nodeDN != null) { ec = entryContainers.get(nodeDN); if (ec == null) { nodeDN = nodeDN.getParentDNInSuffix(); } } return ec; } /** * Get the environment stats of the JE environment used in this root * container. * * @param statsConfig The configuration to use for the EnvironmentStats * object. * @return The environment status of the JE environment. * @throws DatabaseException If an error occurs while retriving the stats * object. */ public EnvironmentStats getEnvironmentStats(StatsConfig statsConfig) throws DatabaseException { return env.getStats(statsConfig); } /** * Get the environment config of the JE environment used in this root * container. * * @return The environment config of the JE environment. * @throws DatabaseException If an error occurs while retriving the * configuration object. */ public EnvironmentConfig getEnvironmentConfig() throws DatabaseException { return env.getConfig(); } /** * Get the total number of entries in this root container. * * @return The number of entries in this root container * @throws DatabaseException If an error occurs while retriving the entry * count. */ public long getEntryCount() throws DatabaseException { long entryCount = 0; for(EntryContainer ec : this.entryContainers.values()) { entryCount += ec.getEntryCount(); } return entryCount; } /** * Assign the next entry ID. * * @return The assigned entry ID. */ public EntryID getNextEntryID() { return new EntryID(nextid.getAndIncrement()); } /** * Return the lowest entry ID assigned. * * @return The lowest entry ID assigned. */ public Long getLowestEntryID() { return 1L; } /** * Return the highest entry ID assigned. * * @return The highest entry ID assigned. */ public Long getHighestEntryID() { return (nextid.get() - 1); } /** * {@inheritDoc} */ public boolean isConfigurationChangeAcceptable( JEBackendCfg cfg, List unacceptableReasons) { boolean acceptable = true; // This listener handles only the changes to JE properties. try { ConfigurableEnvironment.parseConfigEntry(cfg); } catch (Exception e) { unacceptableReasons.add(e.getMessage()); acceptable = false; } return acceptable; } /** * {@inheritDoc} */ public ConfigChangeResult applyConfigurationChange(JEBackendCfg cfg) { ConfigChangeResult ccr; boolean adminActionRequired = false; ArrayList messages = new ArrayList(); try { // Check if any JE non-mutable properties were changed. EnvironmentConfig oldEnvConfig = env.getConfig(); EnvironmentConfig newEnvConfig = ConfigurableEnvironment.parseConfigEntry(cfg); Map paramsMap = EnvironmentParams.SUPPORTED_PARAMS; for (Object o : paramsMap.values()) { ConfigParam param = (ConfigParam) o; if (!param.isMutable()) { String oldValue = oldEnvConfig.getConfigParam(param.getName()); String newValue = newEnvConfig.getConfigParam(param.getName()); if (!oldValue.equalsIgnoreCase(newValue)) { adminActionRequired = true; String configAttr = ConfigurableEnvironment. getAttributeForProperty(param.getName()); if (configAttr != null) { int msgID = MSGID_JEB_CONFIG_ATTR_REQUIRES_RESTART; messages.add(getMessage(msgID, configAttr)); } if(debugEnabled()) { debugInfo("The change to the following property will " + "take effect when the backend is restarted: " + param.getName()); } } } } // This takes care of changes to the JE environment for those // properties that are mutable at runtime. env.setMutableConfig(newEnvConfig); if (debugEnabled()) { debugInfo(env.getConfig().toString()); } } catch (Exception e) { messages.add(e.getMessage()); ccr = new ConfigChangeResult(DirectoryServer.getServerErrorResultCode(), adminActionRequired, messages); return ccr; } ccr = new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); return ccr; } }