/*
* 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
* 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
*
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2011-2013 ForgeRock AS
*/
/**
* This class defines a Directory Server entry cache that uses JE database to
* keep track of the entries. Intended use is when JE database resides in the
* memory based file system which has obvious performance benefits, although
* any file system will do for this cache to function. Entries are maintained
* either by FIFO (default) or LRU (configurable) based list implementation.
* <BR><BR>
* Cache sizing is based on the size of free space available in the file
* system, such that if enough memory is free, then adding an entry to the
* cache will not require purging, but if more than a specified size of the
* file system available space is already consumed, then one or more entries
* will need to be removed in order to make room for a new entry. It is also
* possible to configure a maximum number of entries for the cache. If this
* is specified, then the number of entries will not be allowed to exceed
* this value, but it may not be possible to hold this many entries if the
* available memory fills up first.
* <BR><BR>
* Other configurable parameters for this cache include the maximum length of
* time to block while waiting to acquire a lock, and a set of filters that may
* be used to define criteria for determining which entries are stored in the
* cache. If a filter list is provided, then only entries matching at least
* one of the given filters will be stored in the cache.
* <BR><BR>
* JE environment cache size can also be configured either as percentage of
* the free memory available in the JVM or as explicit size in bytes.
* <BR><BR>
* This cache has a persistence property which, if enabled, allows for the
* contents of the cache to stay persistent across server or cache restarts.
*/
public class FileSystemEntryCache
extends EntryCache <FileSystemEntryCacheCfg>
implements ConfigurationChangeListener <FileSystemEntryCacheCfg> {
/**
* The tracer object for the debug logger.
*/
// Permissions for cache db environment.
new FilePermission(0700);
// The maximum amount of space in bytes that can be consumed in the filesystem
// before we need to start purging entries.
private long maxAllowedMemory;
// The maximum number of entries that may be held in the cache.
// Atomic for additional safety and in case we decide to push
// some locks further down later. Does not inhere in additional
// overhead, via blocking on synchronization primitive, on most
// modern platforms being implemented via cpu instruction set.
// The entry cache home folder to host db environment.
// The type of this cache.
// It can be either FIFO (default) or LRU (configurable).
// This regulates whether we persist the cache across restarts or not.
private boolean persistentCache;
// The lock used to provide threadsafe access when changing the contents
// of the cache maps.
// Entry Cache Index.
// Access order for this cache. FIFO by default.
boolean accessOrder = false;
// JE environment and database related fields for this cache.
// Statistics retrieval operation config for this JE environment.
// The main entry cache database.
// Class database, catalog and binding for serialization.
// JE naming constants.
// The configuration to use when encoding entries in the database.
new EntryEncodeConfig(true, true, true);
// JE native properties to configuration attributes map.
// Currently registered configuration object.
// The maximum length of time to try to obtain a lock before giving
// up.
/**
* Creates a new instance of this entry cache.
*/
public FileSystemEntryCache() {
super();
// Register all JE native properties that map to
// corresponding config attributes.
// All initialization should be performed in the initializeEntryCache.
}
/**
* {@inheritDoc}
*/
throws ConfigException, InitializationException {
// Read and apply configuration.
boolean applyChanges = true;
);
if (!errorMessages.isEmpty()) {
}
}
throw new ConfigException(message);
}
// Set the cache type.
accessOrder = true;
} else {
// Admin framework should only allow for either FIFO or LRU but
// we set the type to default here explicitly if it is not LRU.
accessOrder = false;
}
// Initialize the index.
// Initialize locks.
cacheLock = new ReentrantReadWriteLock(true);
if (accessOrder) {
// In access-ordered linked hash maps, merely querying the map
// with get() is a structural modification.
} else {
}
// Setup the cache home.
try {
} catch (Exception e) {
if (debugEnabled()) {
}
// Not having any home directory for the cache db environment is a
// fatal error as we are unable to continue any further without it.
throw new InitializationException(message, e);
}
// Configure and open JE environment and cache database.
try {
entryCacheDBConfig = new DatabaseConfig();
entryCacheDBConfig.setAllowCreate(true);
// Configure the JE environment statistics to return only
// the values which do not incur some performance penalty.
entryCacheEnvStatsConfig.setFast(true);
// Remove old cache databases if this cache is not persistent.
if ( !persistentCache ) {
try {
} catch (DatabaseNotFoundException e) {}
try {
} catch (DatabaseNotFoundException e) {}
}
// Instantiate the class catalog
//This line causes an unchecked call error if the SuppressWarnings
//annotation is removed at the beginning of this method.
classCatalog, FileSystemEntryCacheIndex.class);
// Get the root configuration object.
// Restoration is static and not subject to the current configuration
// constraints so that the persistent state is truly preserved and
// restored to the exact same state where we left off when the cache
// has been made persistent. The only exception to this is the backend
// offline state matching where entries that belong to backend which
// we cannot match offline state for are discarded from the cache.
if ( persistentCache &&
// If preload is requested there is no point restoring the cache.
).isEntryCachePreload()) {
// Retrieve cache index.
try {
// Persistent state report.
if (OperationStatus.SUCCESS ==
} else {
throw new CacheIndexNotFoundException();
}
// Check cache index state.
throw new CacheIndexImpairedException();
} else {
// Restore entry cache maps from this index.
// Push maxEntries and make it unlimited til restoration complete.
// Compare last known offline states to offline states on startup.
while (offlineBackendIterator.hasNext()) {
// Remove cache entries specific to this backend.
// Log an error message.
}
}
// Pop max entries limit.
}
// Persistent state report.
} catch (CacheIndexNotFoundException e) {
if (debugEnabled()) {
}
// Log an error message.
// Clear the entry cache.
clear();
} catch (CacheIndexImpairedException e) {
if (debugEnabled()) {
}
// Log an error message.
// Clear the entry cache.
clear();
} catch (Exception e) {
if (debugEnabled()) {
}
// Log an error message.
// Clear the entry cache.
clear();
}
}
} catch (Exception e) {
// If we got here it means we have failed to have a proper backend
// for this entry cache and there is absolutely no point going any
// farther from here.
if (debugEnabled()) {
}
throw new InitializationException(message, e);
}
}
/**
* {@inheritDoc}
*/
public void finalizeEntryCache() {
try {
// already exist at this point all we have to do is to serialize cache
// index maps @see FileSystemEntryCacheIndex and put them under indexkey
// allowing for the index to be restored and cache contents reused upon
// the next initialization. If this cache is empty skip persisting phase.
// There must be at least one backend at this stage.
// Store the index.
try {
// Persistent state save report.
//This line causes an unchecked call error if the SuppressWarnings
//annotation is removed at the beginning of this method.
indexData)) {
throw new Exception();
}
} catch (Exception e) {
if (debugEnabled()) {
}
// Log an error message.
}
// Persistent state save report.
}
// Close JE databases and environment and clear all the maps.
try {
if (entryCacheDB != null) {
}
if (entryCacheClassDB != null) {
}
if (entryCacheEnv != null) {
// Remove cache and index dbs if this cache is not persistent.
if (!persistentCache) {
try {
} catch (DatabaseNotFoundException e) {}
try {
} catch (DatabaseNotFoundException e) {}
}
}
} catch (Exception e) {
if (debugEnabled()) {
}
// That is ok, JE verification and repair on startup should take care of
// this so if there are any unrecoverable errors during next startup
// and we are unable to handle and cleanup them we will log errors then.
}
} finally {
}
}
/**
* {@inheritDoc}
*/
{
return false;
}
// Indicate whether the DN map contains the specified DN.
boolean containsEntry = false;
try {
} finally {
}
return containsEntry;
}
/**
* {@inheritDoc}
*/
// Get the entry from the DN map if it is present. If not, then return
// null.
try {
// Use get to generate entry access.
// Indicate cache hit.
} else {
// Indicate cache miss.
}
} finally {
}
return entry;
}
/**
* {@inheritDoc}
*/
long entryID = -1;
try {
}
} finally {
}
return entryID;
}
/**
* {@inheritDoc}
*/
try {
// Get the map for the provided backend. If it isn't present, then
// return null.
.getBackendID());
{
// Get the entry DN from the map by its ID. If it isn't present,
// then return null.
}
} catch (Exception e) {
// Ignore.
} finally {
}
return entryDN;
}
/**
* {@inheritDoc}
*/
{
try {
// TODO: Cache the buffer?
} catch (Exception e) {
if (debugEnabled()) {
}
}
}
/**
* {@inheritDoc}
*/
{
try {
// See if the entry already exists in the cache. If it does, then we
// will fail and not actually store the entry.
return false;
}
} finally {
}
try {
// TODO: Cache the buffer?
} catch (Exception e) {
if (debugEnabled()) {
}
// We can't rule out the possibility of a conflict, so return false.
return false;
}
}
/**
* {@inheritDoc}
*/
try {
return;
}
while (backendIterator.hasNext()) {
backendIterator.next());
// If this backend becomes empty now
// remove it from the backend map.
}
break;
}
}
} catch (Exception e) {
if (debugEnabled()) {
}
} finally {
}
}
/**
* {@inheritDoc}
*/
public void clear() {
try {
try {
// Instantiate the class catalog
//This line causes an unchecked call error if the SuppressWarnings
//annotation is removed at the beginning of this method.
classCatalog, FileSystemEntryCacheIndex.class);
}
} catch (Exception e) {
if (debugEnabled()) {
}
}
} finally {
}
}
/**
* {@inheritDoc}
*/
try {
try {
if (backendEntriesMap == null) {
// No entries were in the cache for this backend,
// so we can return without doing anything.
return;
}
int entriesExamined = 0;
while (backendEntriesIterator.hasNext()) {
// This can take a while, so we'll periodically release and
// re-acquire the lock in case anyone else is waiting on it
// so this doesn't become a stop-the-world event as far as
// the cache is concerned.
}
}
// This backend is empty now, remove it from the backend map.
} catch (Exception e) {
if (debugEnabled()) {
}
}
} finally {
}
}
/**
* {@inheritDoc}
*/
// Determine which backend should be used for the provided base DN. If
// there is none, then we don't need to do anything.
{
return;
}
// Acquire a lock on the cache. We should not return until the cache has
// been cleared, so we will block until we can obtain the lock.
// At this point, it is absolutely critical that we always release the lock
// before leaving this method, so do so in a finally block.
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
// This shouldn't happen, but there's not much that we can do if it does.
}
finally
{
}
}
/**
* Clears all entries at or below the specified base DN that are associated
* with the given backend. The caller must already hold the cache lock.
*
* @param baseDN The base DN below which all entries should be flushed.
* @param backend The backend for which to remove the appropriate entries.
*/
// See if there are any entries for the provided backend in the cache. If
// not, then return.
{
// No entries were in the cache for this backend, so we can return without
// doing anything.
return;
}
// Since the provided base DN could hold a subset of the information in the
// specified backend, we will have to do this by iterating through all the
// entries for that backend. Since this could take a while, we'll
// periodically release and re-acquire the lock in case anyone else is
// waiting on it so this doesn't become a stop-the-world event as far as the
// cache is concerned.
int entriesExamined = 0;
{
try {
try {
new DatabaseEntry(
} catch (Exception e) {
if (debugEnabled()) {
}
}
}
}
} catch (Exception e) {
// Ignore.
}
}
// If this backend becomes empty now
// remove it from the backend map.
}
// See if the backend has any subordinate backends. If so, then process
// them recursively.
{
boolean isAppropriate = false;
{
{
isAppropriate = true;
break;
}
}
if (isAppropriate)
{
}
}
}
/**
* {@inheritDoc}
*/
public void handleLowMemory() {
// This is about all we can do.
if (entryCacheEnv != null) {
try {
// Free some JVM memory.
} catch (Exception e) {
if (debugEnabled()) {
}
}
}
}
/**
* {@inheritDoc}
*/
@Override()
{
}
/**
* {@inheritDoc}
*/
public boolean isConfigurationChangeAcceptable(
)
{
boolean applyChanges = false;
);
return errorHandler.getIsAcceptable();
}
/**
* {@inheritDoc}
*/
)
{
boolean applyChanges = true;
);
// Do not apply changes unless this cache is enabled.
if (configuration.isEnabled()) {
}
);
return changeResult;
}
/**
* Parses the provided configuration and configure the entry cache.
*
* @param configuration The new configuration containing the changes.
* @param applyChanges If true then take into account the new configuration.
* @param errorHandler An handler used to report errors.
*
* @return <CODE>true</CODE> if configuration is acceptable,
* or <CODE>false</CODE> otherwise.
*/
public boolean processEntryCacheConfig(
boolean applyChanges,
)
{
// Local variables to read configuration.
long newLockTimeout;
long newMaxEntries;
long newMaxAllowedMemory;
int newJECachePercent;
long newJECacheSize;
boolean newPersistentCache;
boolean newCompactEncoding;
new EnvironmentMutableConfig();
new EnvironmentConfig();
// Read configuration.
// If the value of zero arrives make sure it is traslated
// to the maximum possible value we can cap maxEntries to.
if (newMaxEntries <= 0) {
}
// Determine JE cache percent.
// Determine JE cache size.
// Check if this cache is persistent.
// Check if this cache should use compact encoding.
// Get native JE properties.
switch (errorHandler.getConfigPhase())
{
case PHASE_INIT:
// Determine the cache type.
// Determine the cache home.
);
);
// JE configuration properties.
try {
} catch (Exception e) {
if (debugEnabled()) {
}
false,
);
}
try {
} catch (Exception e) {
if (debugEnabled()) {
}
false,
);
}
// JE native properties.
try {
} catch (Exception e) {
if (debugEnabled()) {
}
false, DirectoryServer.getServerErrorResultCode());
}
break;
case PHASE_ACCEPTABLE: // acceptable and apply are using the same
case PHASE_APPLY: // error ID codes
);
);
// Iterate through native JE properties.
try {
// If this entry cache is disabled then there is no open JE
// environment to check against, skip mutable check if so.
if (configuration.isEnabled()) {
// There is no need to validate properties yet again.
true);
if (debugEnabled()) {
"will take effect when the component is restarted: " +
}
}
}
}
}
} else {
}
} catch (ConfigException ce) {
false, DirectoryServer.getServerErrorResultCode());
} catch (Exception e) {
false, DirectoryServer.getServerErrorResultCode());
}
break;
}
{
switch (errorHandler.getConfigPhase()) {
case PHASE_INIT:
break;
case PHASE_APPLY:
try {
} catch (Exception e) {
if (debugEnabled()) {
}
false,
);
}
try {
} catch (Exception e) {
if (debugEnabled()) {
}
false,
);
}
try {
// This takes care of changes to the JE environment for those
// properties that are mutable at runtime.
} catch (Exception e) {
if (debugEnabled()) {
}
false,
);
}
break;
}
encodeConfig = new EntryEncodeConfig(true,
}
return errorHandler.getIsAcceptable();
}
/**
* {@inheritDoc}
*/
{
try {
// If cache misses is maintained by default cache
// get it from there and if not point to itself.
new Long(maxAllowedMemory),
);
} catch (Exception e) {
if (debugEnabled()) {
}
}
return attrs;
}
/**
* {@inheritDoc}
*/
{
}
/**
* Retrieves and decodes the entry with the specified DN from JE backend db.
*
* @param entryDN The DN of the entry to retrieve.
*
* @return The requested entry if it is present in the cache, or
* <CODE>null</CODE> if it is not present.
*/
{
try {
// Get the primary key and data.
return entry;
} else {
throw new Exception();
}
} catch (Exception e) {
if (debugEnabled()) {
}
// Log an error message.
}
return null;
}
/**
* Encodes and stores the entry in the JE backend db.
*
* @param backend The backend with which the entry is associated.
* @param entryID The entry ID within the provided backend that uniquely
* identifies the specified entry.
*
* @return <CODE>false</CODE> if some problem prevented the method from
* completing successfully, or <CODE>true</CODE> if the entry
* was either stored or the cache determined that this entry
* should never be cached for some reason.
*/
long entryID,
try {
// Obtain a lock on the cache. If this fails, then don't do anything.
return false;
}
// See if the current fs space usage is within acceptable constraints. If
// so, then add the entry to the cache (or replace it if it is already
// present). If not, then remove an existing entry and don't add the new
// entry.
// Zero means unlimited here.
if (maxAllowedMemory != 0) {
// Get approximate current total log size of JE environment in bytes.
long usedMemory =
// TODO: Check and log a warning if usedMemory hits default or
// configurable watermark, see Issue 1735.
if (usedMemory > maxAllowedMemory) {
// Cap maxEntries artificially but dont let it go negative under
// any circumstances.
// Add the entry to the map to trigger remove of the eldest entry.
// @see LinkedHashMapRotator.removeEldestEntry() for more details.
// Restore the map and maxEntries.
// We'll always return true in this case, even tho we didn't actually
// add the entry due to memory constraints.
return true;
}
}
// Create key.
// Create data and put this cache entry into the database.
// Add the entry to the cache index maps.
} else {
}
}
// We'll always return true in this case, even if we didn't actually add
// the entry due to memory constraints.
return true;
} catch (Exception e) {
if (debugEnabled()) {
}
// Log an error message.
return false;
} finally {
if (cacheLock.isWriteLockedByCurrentThread()) {
}
}
}
/**
* Checks if the cache home exist and if not tries to recursively create it.
* If either is successful adjusts cache home access permissions accordingly
* to allow only process owner or the superuser to access JE environment.
*
* @param cacheHome String representation of complete file system path.
*
* @throws Exception If failed to establish cache home.
*/
boolean cacheHasHome;
if (cacheHomeDir.exists() &&
cacheHomeDir.canRead() &&
cacheHomeDir.canWrite()) {
cacheHasHome = true;
} else {
try {
} catch (SecurityException e) {
cacheHasHome = false;
}
}
if ( cacheHasHome ) {
// TODO: Investigate if its feasible to employ SetFileAttributes()
// FILE_ATTRIBUTE_TEMPORARY attribute on Windows via native code.
if(FilePermission.canSetPermissions()) {
try {
throw new Exception();
}
} catch(Exception e) {
// Log a warning that the permissions were not set.
}
}
} else {
throw new Exception();
}
}
/**
* {@inheritDoc}
*/
{
// Grab write lock to prevent any modifications
// to the cache maps until a snapshot is taken.
try {
// Examining the real maps will hold the lock
// and can cause map modifications in case of
// any access order maps, make copies instead.
} finally {
}
// Check dnMap first.
while (backendIterator.hasNext()) {
break;
}
}
}
// See if there is anything on backendMap that isnt reflected on dnMap
// in case maps went out of sync.
while (backendIterator.hasNext()) {
}
}
}
}
/**
* The eldest entry is selected by the LinkedHashMap implementation based
* on the access order configured.
*
* @param eldest The least recently inserted entry in the map, or if
* this is an access-ordered map, the least recently
* accessed entry. This is the entry that will be
* removed it this method returns true. If the map was
* empty prior to the put or putAll invocation resulting
* in this invocation, this will be the entry that was
* just inserted; in other words, if the map contains a
* single entry, the eldest entry is also the newest.
*
* @return boolean {@code true} if the eldest entry should be removed
* from the map; {@code false} if it should be retained.
*/
// Check if we hit the limit on max entries and if so remove
// the eldest entry otherwise do nothing.
try {
// Remove the the eldest entry from supporting maps.
while (backendIterator.hasNext()) {
backendIterator.next());
// If this backend becomes empty now
// remove it from the backend map.
}
break;
}
}
// Remove the the eldest entry from the database.
} catch (Exception e) {
if (debugEnabled()) {
}
} finally {
}
return true;
} else {
return false;
}
}
/**
* This exception should be thrown if an error occurs while
* trying to locate and load persistent cache index from
* the existing entry cache database.
*/
public CacheIndexNotFoundException() {}
}
/**
* This exception should be thrown if persistent cache index
* found in the existing entry cache database is determined
* to be empty, inconsistent or damaged.
*/
public CacheIndexImpairedException() {}
}
}