/*
* 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 legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* 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 legal-notices/CDDLv1_0.txt.
* 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 2015 ForgeRock AS
*/
package org.opends.server.backends.jeb;
import static com.sleepycat.je.EnvironmentConfig.*;
import static com.sleepycat.je.LockMode.READ_COMMITTED;
import static com.sleepycat.je.LockMode.RMW;
import static com.sleepycat.je.OperationStatus.*;
import static org.forgerock.util.Utils.*;
import static org.opends.messages.BackendMessages.*;
import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.backends.pluggable.spi.StorageUtils.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.util.Reject;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.JEBackendCfg;
import org.opends.server.api.Backupable;
import org.opends.server.api.DiskSpaceMonitorHandler;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.Importer;
import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.SequentialCursor;
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.StorageStatus;
import org.opends.server.backends.pluggable.spi.StorageUtils;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.UpdateFunction;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.MemoryQuota;
import org.opends.server.core.ServerContext;
import org.opends.server.extensions.DiskSpaceMonitor;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.RestoreConfig;
import org.opends.server.util.BackupManager;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.DatabaseNotFoundException;
import com.sleepycat.je.Durability;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
/** Berkeley DB Java Edition (JE for short) database implementation of the {@link Storage} engine. */
public final class JEStorage implements Storage, Backupable, ConfigurationChangeListener<JEBackendCfg>,
DiskSpaceMonitorHandler
{
/** JE implementation of the {@link Cursor} interface. */
private static final class CursorImpl implements Cursor<ByteString, ByteString>
{
private ByteString currentKey;
private ByteString currentValue;
private boolean isDefined;
private final com.sleepycat.je.Cursor cursor;
private final DatabaseEntry dbKey = new DatabaseEntry();
private final DatabaseEntry dbValue = new DatabaseEntry();
private CursorImpl(com.sleepycat.je.Cursor cursor)
{
this.cursor = cursor;
}
@Override
public void close()
{
closeSilently(cursor);
}
@Override
public boolean isDefined()
{
return isDefined;
}
@Override
public ByteString getKey()
{
if (currentKey == null)
{
throwIfNotSuccess();
currentKey = ByteString.wrap(dbKey.getData());
}
return currentKey;
}
@Override
public ByteString getValue()
{
if (currentValue == null)
{
throwIfNotSuccess();
currentValue = ByteString.wrap(dbValue.getData());
}
return currentValue;
}
@Override
public boolean next()
{
clearCurrentKeyAndValue();
try
{
isDefined = cursor.getNext(dbKey, dbValue, null) == SUCCESS;
return isDefined;
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public void delete() throws NoSuchElementException, UnsupportedOperationException
{
throwIfNotSuccess();
try
{
cursor.delete();
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public boolean positionToKey(final ByteSequence key)
{
clearCurrentKeyAndValue();
setData(dbKey, key);
try
{
isDefined = cursor.getSearchKey(dbKey, dbValue, null) == SUCCESS;
return isDefined;
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public boolean positionToKeyOrNext(final ByteSequence key)
{
clearCurrentKeyAndValue();
setData(dbKey, key);
try
{
isDefined = cursor.getSearchKeyRange(dbKey, dbValue, null) == SUCCESS;
return isDefined;
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public boolean positionToIndex(int index)
{
clearCurrentKeyAndValue();
try
{
isDefined = cursor.getFirst(dbKey, dbValue, null) == SUCCESS;
if (!isDefined)
{
return false;
}
else if (index == 0)
{
return true;
}
// equivalent to READ_UNCOMMITTED
long skipped = cursor.skipNext(index, dbKey, dbValue, null);
if (skipped == index)
{
isDefined = cursor.getCurrent(dbKey, dbValue, null) == SUCCESS;
}
else
{
isDefined = false;
}
return isDefined;
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public boolean positionToLastKey()
{
clearCurrentKeyAndValue();
try
{
isDefined = cursor.getLast(dbKey, dbValue, null) == SUCCESS;
return isDefined;
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
private void clearCurrentKeyAndValue()
{
currentKey = null;
currentValue = null;
}
private void throwIfNotSuccess()
{
if (!isDefined())
{
throw new NoSuchElementException();
}
}
}
/** JE implementation of the {@link Importer} interface. */
private final class ImporterImpl implements Importer
{
private final Map<TreeName, Database> trees = new HashMap<>();
private Database getOrOpenTree(TreeName treeName)
{
return getOrOpenTree0(null, trees, treeName);
}
@Override
public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value)
{
try
{
getOrOpenTree(treeName).put(null, db(key), db(value));
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public ByteString read(final TreeName treeName, final ByteSequence key)
{
try
{
DatabaseEntry dbValue = new DatabaseEntry();
boolean isDefined = getOrOpenTree(treeName).get(null, db(key), dbValue, null) == SUCCESS;
return valueToBytes(dbValue, isDefined);
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public SequentialCursor<ByteString, ByteString> openCursor(TreeName treeName)
{
try
{
return new CursorImpl(getOrOpenTree(treeName).openCursor(null, new CursorConfig()));
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public void clearTree(TreeName treeName)
{
env.truncateDatabase(null, toDatabaseName(treeName), false);
}
@Override
public void close()
{
closeSilently(trees.values());
trees.clear();
JEStorage.this.close();
}
}
/** JE implementation of the {@link WriteableTransaction} interface. */
private final class WriteableTransactionImpl implements WriteableTransaction
{
private final Transaction txn;
private WriteableTransactionImpl(Transaction txn)
{
this.txn = txn;
}
/**
* This is currently needed for import-ldif:
* <ol>
* <li>Opening the EntryContainer calls {@link #openTree(TreeName, boolean)} for each index</li>
* <li>Then the underlying storage is closed</li>
* <li>Then {@link Importer#startImport()} is called</li>
* <li>Then ID2Entry#put() is called</li>
* <li>Which in turn calls ID2Entry#encodeEntry()</li>
* <li>Which in turn finally calls PersistentCompressedSchema#store()</li>
* <li>Which uses a reference to the storage (that was closed before calling startImport()) and
* uses it as if it was open</li>
* </ol>
*/
private Database getOrOpenTree(TreeName treeName)
{
try
{
return getOrOpenTree0(txn, trees, treeName);
}
catch (Exception e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value)
{
try
{
final OperationStatus status = getOrOpenTree(treeName).put(txn, db(key), db(value));
if (status != SUCCESS)
{
throw new StorageRuntimeException(putErrorMsg(treeName, key, value, "did not succeed: " + status));
}
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(putErrorMsg(treeName, key, value, "threw an exception"), e);
}
}
private String putErrorMsg(TreeName treeName, ByteSequence key, ByteSequence value, String msg)
{
return "put(treeName=" + treeName + ", key=" + key + ", value=" + value + ") " + msg;
}
@Override
public boolean delete(final TreeName treeName, final ByteSequence key)
{
try
{
return getOrOpenTree(treeName).delete(txn, db(key)) == SUCCESS;
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(deleteErrorMsg(treeName, key, "threw an exception"), e);
}
}
private String deleteErrorMsg(TreeName treeName, ByteSequence key, String msg)
{
return "delete(treeName=" + treeName + ", key=" + key + ") " + msg;
}
@Override
public long getRecordCount(TreeName treeName)
{
try
{
return getOrOpenTree(treeName).count();
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public Cursor<ByteString, ByteString> openCursor(final TreeName treeName)
{
try
{
return new CursorImpl(getOrOpenTree(treeName).openCursor(txn, CursorConfig.READ_COMMITTED));
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public ByteString read(final TreeName treeName, final ByteSequence key)
{
try
{
DatabaseEntry dbValue = new DatabaseEntry();
boolean isDefined = getOrOpenTree(treeName).get(txn, db(key), dbValue, READ_COMMITTED) == SUCCESS;
return valueToBytes(dbValue, isDefined);
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public boolean update(final TreeName treeName, final ByteSequence key, final UpdateFunction f)
{
try
{
final Database tree = getOrOpenTree(treeName);
final DatabaseEntry dbKey = db(key);
final DatabaseEntry dbValue = new DatabaseEntry();
for (;;)
{
final boolean isDefined = tree.get(txn, dbKey, dbValue, RMW) == SUCCESS;
final ByteSequence oldValue = valueToBytes(dbValue, isDefined);
final ByteSequence newValue = f.computeNewValue(oldValue);
if (Objects.equals(newValue, oldValue))
{
return false;
}
if (newValue == null)
{
return tree.delete(txn, dbKey) == SUCCESS;
}
setData(dbValue, newValue);
if (isDefined)
{
return tree.put(txn, dbKey, dbValue) == SUCCESS;
}
else if (tree.putNoOverwrite(txn, dbKey, dbValue) == SUCCESS)
{
return true;
}
// else retry due to phantom read: another thread inserted a record
}
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public void openTree(final TreeName treeName, boolean createOnDemand)
{
getOrOpenTree(treeName);
}
@Override
public void deleteTree(final TreeName treeName)
{
try
{
synchronized (trees)
{
closeSilently(trees.remove(treeName));
env.removeDatabase(txn, toDatabaseName(treeName));
}
}
catch (DatabaseNotFoundException e)
{
// This is fine: end result is what we wanted
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
}
/** JE read-only implementation of {@link StorageImpl} interface. */
private final class ReadOnlyTransactionImpl implements WriteableTransaction
{
private final WriteableTransactionImpl delegate;
ReadOnlyTransactionImpl(WriteableTransactionImpl delegate)
{
this.delegate = delegate;
}
@Override
public ByteString read(TreeName treeName, ByteSequence key)
{
return delegate.read(treeName, key);
}
@Override
public Cursor<ByteString, ByteString> openCursor(TreeName treeName)
{
return delegate.openCursor(treeName);
}
@Override
public long getRecordCount(TreeName treeName)
{
return delegate.getRecordCount(treeName);
}
@Override
public void openTree(TreeName treeName, boolean createOnDemand)
{
if (createOnDemand)
{
throw new ReadOnlyStorageException();
}
delegate.openTree(treeName, false);
}
@Override
public void deleteTree(TreeName name)
{
throw new ReadOnlyStorageException();
}
@Override
public void put(TreeName treeName, ByteSequence key, ByteSequence value)
{
throw new ReadOnlyStorageException();
}
@Override
public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f)
{
throw new ReadOnlyStorageException();
}
@Override
public boolean delete(TreeName treeName, ByteSequence key)
{
throw new ReadOnlyStorageException();
}
}
private WriteableTransaction newWriteableTransaction(Transaction txn)
{
final WriteableTransactionImpl writeableStorage = new WriteableTransactionImpl(txn);
return accessMode.isWriteable() ? writeableStorage : new ReadOnlyTransactionImpl(writeableStorage);
}
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** Use read committed isolation instead of the default which is repeatable read. */
private static final TransactionConfig TXN_READ_COMMITTED = new TransactionConfig().setReadCommitted(true);
private final ServerContext serverContext;
private final File backendDirectory;
private JEBackendCfg config;
private AccessMode accessMode;
private Environment env;
private EnvironmentConfig envConfig;
private MemoryQuota memQuota;
private JEMonitor monitor;
private DiskSpaceMonitor diskMonitor;
private StorageStatus storageStatus = StorageStatus.working();
private final ConcurrentMap<TreeName, Database> trees = new ConcurrentHashMap<>();
/**
* Creates a new JE storage with the provided configuration.
*
* @param cfg
* The configuration.
* @param serverContext
* This server instance context
* @throws ConfigException
* if memory cannot be reserved
*/
JEStorage(final JEBackendCfg cfg, ServerContext serverContext) throws ConfigException
{
this.serverContext = serverContext;
backendDirectory = getBackendDirectory(cfg);
config = cfg;
cfg.addJEChangeListener(this);
}
private Database getOrOpenTree0(Transaction txn, Map<TreeName, Database> trees, TreeName treeName)
{
Database tree = trees.get(treeName);
if (tree == null)
{
synchronized (trees)
{
tree = trees.get(treeName);
if (tree == null)
{
tree = env.openDatabase(null, toDatabaseName(treeName), dbConfig());
trees.put(treeName, tree);
}
}
}
return tree;
}
private void buildConfiguration(AccessMode accessMode, boolean isImport) throws ConfigException
{
this.accessMode = accessMode;
if (isImport)
{
envConfig = new EnvironmentConfig();
envConfig
.setTransactional(false)
.setAllowCreate(true)
.setLockTimeout(0, TimeUnit.SECONDS)
.setTxnTimeout(0, TimeUnit.SECONDS)
.setDurability(Durability.COMMIT_NO_SYNC)
.setConfigParam(CLEANER_MIN_UTILIZATION, String.valueOf(config.getDBCleanerMinUtilization()))
.setConfigParam(LOG_FILE_MAX, String.valueOf(config.getDBLogFileMax()));
}
else
{
envConfig = ConfigurableEnvironment.parseConfigEntry(config);
}
diskMonitor = serverContext.getDiskSpaceMonitor();
memQuota = serverContext.getMemoryQuota();
if (config.getDBCacheSize() > 0)
{
memQuota.acquireMemory(config.getDBCacheSize());
}
else
{
memQuota.acquireMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
}
}
private DatabaseConfig dbConfig()
{
boolean isImport = !envConfig.getTransactional();
return new DatabaseConfig()
.setKeyPrefixing(true)
.setAllowCreate(true)
.setTransactional(!isImport)
.setDeferredWrite(isImport);
}
@Override
public void close()
{
synchronized (trees)
{
closeSilently(trees.values());
trees.clear();
}
if (env != null)
{
DirectoryServer.deregisterMonitorProvider(monitor);
monitor = null;
try
{
env.close();
env = null;
}
catch (DatabaseException e)
{
throw new IllegalStateException(e);
}
}
if (config.getDBCacheSize() > 0)
{
memQuota.releaseMemory(config.getDBCacheSize());
}
else
{
memQuota.releaseMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
}
config.removeJEChangeListener(this);
diskMonitor.deregisterMonitoredDirectory(getDirectory(), this);
}
@Override
public void open(AccessMode accessMode) throws ConfigException, StorageRuntimeException
{
Reject.ifNull(accessMode, "accessMode must not be null");
buildConfiguration(accessMode, false);
open0();
}
private void open0() throws ConfigException
{
setupStorageFiles(backendDirectory, config.getDBDirectoryPermissions(), config.dn());
try
{
if (env != null)
{
throw new IllegalStateException(
"Database is already open, either the backend is enabled or an import is currently running.");
}
env = new Environment(backendDirectory, envConfig);
monitor = new JEMonitor(config.getBackendId() + " JE Database", env);
DirectoryServer.registerMonitorProvider(monitor);
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
registerMonitoredDirectory(config);
}
@Override
public <T> T read(final ReadOperation<T> operation) throws Exception
{
try
{
return operation.run(newWriteableTransaction(null));
}
catch (final StorageRuntimeException e)
{
if (e.getCause() != null)
{
throw (Exception) e.getCause();
}
throw e;
}
}
@Override
public Importer startImport() throws ConfigException, StorageRuntimeException
{
buildConfiguration(AccessMode.READ_WRITE, true);
open0();
return new ImporterImpl();
}
private static String toDatabaseName(final TreeName treeName)
{
return treeName.toString();
}
@Override
public void write(final WriteOperation operation) throws Exception
{
final Transaction txn = beginTransaction();
try
{
operation.run(newWriteableTransaction(txn));
commit(txn);
}
catch (final StorageRuntimeException e)
{
if (e.getCause() != null)
{
throw (Exception) e.getCause();
}
throw e;
}
finally
{
abort(txn);
}
}
private Transaction beginTransaction()
{
if (envConfig.getTransactional())
{
final Transaction txn = env.beginTransaction(null, TXN_READ_COMMITTED);
logger.trace("beginTransaction txnid=%d", txn.getId());
return txn;
}
return null;
}
private void commit(final Transaction txn)
{
if (txn != null)
{
txn.commit();
logger.trace("commit txnid=%d", txn.getId());
}
}
private void abort(final Transaction txn)
{
if (txn != null)
{
txn.abort();
logger.trace("abort txnid=%d", txn.getId());
}
}
@Override
public boolean supportsBackupAndRestore()
{
return true;
}
@Override
public File getDirectory()
{
return getBackendDirectory(config);
}
private static File getBackendDirectory(JEBackendCfg cfg)
{
return getDBDirectory(cfg.getDBDirectory(), cfg.getBackendId());
}
@Override
public ListIterator<Path> getFilesToBackup() throws DirectoryException
{
return new JELogFilesIterator(getDirectory(), config.getBackendId());
}
/**
* Iterator on JE log files to backup.
* <p>
* The cleaner thread may delete some log files during the backup. The iterator is automatically
* renewed if at least one file has been deleted.
*/
static class JELogFilesIterator implements ListIterator<Path>
{
/** Root directory where all files are located. */
private final File rootDirectory;
private final String backendID;
/** Underlying iterator on files. */
private ListIterator<Path> iterator;
/** Files to backup. Used to renew the iterator if necessary. */
private List<Path> files;
private String lastFileName = "";
private long lastFileSize;
JELogFilesIterator(File rootDirectory, String backendID) throws DirectoryException
{
this.rootDirectory = rootDirectory;
this.backendID = backendID;
setFiles(BackupManager.getFiles(rootDirectory, new JELogFileFilter(), backendID));
}
private void setFiles(List<Path> files)
{
this.files = files;
Collections.sort(files);
if (!files.isEmpty())
{
Path lastFile = files.get(files.size() - 1);
lastFileName = lastFile.getFileName().toString();
lastFileSize = lastFile.toFile().length();
}
iterator = files.listIterator();
}
@Override
public boolean hasNext()
{
boolean hasNext = iterator.hasNext();
if (!hasNext && !files.isEmpty())
{
try
{
List<Path> allFiles = BackupManager.getFiles(rootDirectory, new JELogFileFilter(), backendID);
List<Path> compare = new ArrayList<>(files);
compare.removeAll(allFiles);
if (!compare.isEmpty())
{
// at least one file was deleted,
// the iterator must be renewed based on last file previously available
List<Path> newFiles =
BackupManager.getFiles(rootDirectory, new JELogFileFilter(lastFileName, lastFileSize), backendID);
logger.info(NOTE_JEB_BACKUP_CLEANER_ACTIVITY.get(newFiles.size()));
if (!newFiles.isEmpty())
{
setFiles(newFiles);
hasNext = iterator.hasNext();
}
}
}
catch (DirectoryException e)
{
logger.error(ERR_BACKEND_LIST_FILES_TO_BACKUP.get(backendID, stackTraceToSingleLineString(e)));
}
}
return hasNext;
}
@Override
public Path next()
{
if (hasNext())
{
return iterator.next();
}
throw new NoSuchElementException();
}
@Override
public boolean hasPrevious()
{
return iterator.hasPrevious();
}
@Override
public Path previous()
{
return iterator.previous();
}
@Override
public int nextIndex()
{
return iterator.nextIndex();
}
@Override
public int previousIndex()
{
return iterator.previousIndex();
}
@Override
public void remove()
{
throw new UnsupportedOperationException("remove() is not implemented");
}
@Override
public void set(Path e)
{
throw new UnsupportedOperationException("set() is not implemented");
}
@Override
public void add(Path e)
{
throw new UnsupportedOperationException("add() is not implemented");
}
}
/**
* This class implements a FilenameFilter to detect a JE log file, possibly with a constraint on
* the file name and file size.
*/
private static class JELogFileFilter implements FileFilter
{
private final String latestFilename;
private final long latestFileSize;
/**
* Creates the filter for log files that are newer than provided file name
* or equal to provided file name and of larger size.
* @param latestFilename the latest file name
* @param latestFileSize the latest file size
*/
JELogFileFilter(String latestFilename, long latestFileSize)
{
this.latestFilename = latestFilename;
this.latestFileSize = latestFileSize;
}
/** Creates the filter for any JE log file. */
JELogFileFilter()
{
this("", 0);
}
@Override
public boolean accept(File file)
{
String name = file.getName();
int cmp = name.compareTo(latestFilename);
return name.endsWith(".jdb")
&& (cmp > 0 || (cmp == 0 && file.length() > latestFileSize));
}
}
@Override
public Path beforeRestore() throws DirectoryException
{
return null;
}
@Override
public boolean isDirectRestore()
{
// restore is done in an intermediate directory
return false;
}
@Override
public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException
{
// intermediate directory content is moved to database directory
File targetDirectory = getDirectory();
recursiveDelete(targetDirectory);
try
{
Files.move(restoreDirectory, targetDirectory.toPath());
}
catch(IOException e)
{
LocalizableMessage msg = ERR_CANNOT_RENAME_RESTORE_DIRECTORY.get(restoreDirectory, targetDirectory.getPath());
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg);
}
}
@Override
public void createBackup(BackupConfig backupConfig) throws DirectoryException
{
new BackupManager(config.getBackendId()).createBackup(this, backupConfig);
}
@Override
public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
{
new BackupManager(config.getBackendId()).removeBackup(backupDirectory, backupID);
}
@Override
public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
{
new BackupManager(config.getBackendId()).restoreBackup(this, restoreConfig);
}
@Override
public Set<TreeName> listTrees()
{
try
{
List<String> treeNames = env.getDatabaseNames();
final Set<TreeName> results = new HashSet<>(treeNames.size());
for (String treeName : treeNames)
{
results.add(TreeName.valueOf(treeName));
}
return results;
}
catch (DatabaseException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public boolean isConfigurationChangeAcceptable(JEBackendCfg newCfg,
List<LocalizableMessage> unacceptableReasons)
{
long newSize = computeSize(newCfg);
long oldSize = computeSize(config);
return (newSize <= oldSize || memQuota.isMemoryAvailable(newSize - oldSize))
&& checkConfigurationDirectories(newCfg, unacceptableReasons);
}
private long computeSize(JEBackendCfg cfg)
{
return cfg.getDBCacheSize() > 0 ? cfg.getDBCacheSize() : memQuota.memPercentToBytes(cfg.getDBCachePercent());
}
/**
* Checks newly created backend has a valid configuration.
* @param cfg the new configuration
* @param unacceptableReasons the list of accumulated errors and their messages
* @param context the server context
* @return true if newly created backend has a valid configuration
*/
static boolean isConfigurationAcceptable(JEBackendCfg cfg, List<LocalizableMessage> unacceptableReasons,
ServerContext context)
{
if (context != null)
{
MemoryQuota memQuota = context.getMemoryQuota();
if (cfg.getDBCacheSize() > 0 && !memQuota.isMemoryAvailable(cfg.getDBCacheSize()))
{
unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get(
cfg.getDBCacheSize(), memQuota.getAvailableMemory()));
return false;
}
else if (!memQuota.isMemoryAvailable(memQuota.memPercentToBytes(cfg.getDBCachePercent())))
{
unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_PERCENT_GREATER_THAN_JVM_HEAP.get(
cfg.getDBCachePercent(), memQuota.memBytesToPercent(memQuota.getAvailableMemory())));
return false;
}
}
return checkConfigurationDirectories(cfg, unacceptableReasons);
}
private static boolean checkConfigurationDirectories(JEBackendCfg cfg,
List<LocalizableMessage> unacceptableReasons)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
File newBackendDirectory = getBackendDirectory(cfg);
checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, true);
checkDBDirPermissions(cfg.getDBDirectoryPermissions(), cfg.dn(), ccr);
if (!ccr.getMessages().isEmpty())
{
unacceptableReasons.addAll(ccr.getMessages());
return false;
}
return true;
}
@Override
public ConfigChangeResult applyConfigurationChange(JEBackendCfg cfg)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
try
{
File newBackendDirectory = getBackendDirectory(cfg);
// Create the directory if it doesn't exist.
if (!cfg.getDBDirectory().equals(config.getDBDirectory()))
{
checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, false);
if (!ccr.getMessages().isEmpty())
{
return ccr;
}
ccr.setAdminActionRequired(true);
ccr.addMessage(NOTE_CONFIG_DB_DIR_REQUIRES_RESTART.get(config.getDBDirectory(), cfg.getDBDirectory()));
}
if (!cfg.getDBDirectoryPermissions().equalsIgnoreCase(config.getDBDirectoryPermissions())
|| !cfg.getDBDirectory().equals(config.getDBDirectory()))
{
checkDBDirPermissions(cfg.getDBDirectoryPermissions(), cfg.dn(), ccr);
if (!ccr.getMessages().isEmpty())
{
return ccr;
}
setDBDirPermissions(newBackendDirectory, cfg.getDBDirectoryPermissions(), cfg.dn(), ccr);
if (!ccr.getMessages().isEmpty())
{
return ccr;
}
}
registerMonitoredDirectory(cfg);
config = cfg;
}
catch (Exception e)
{
addErrorMessage(ccr, LocalizableMessage.raw(stackTraceToSingleLineString(e)));
}
return ccr;
}
private void registerMonitoredDirectory(JEBackendCfg cfg)
{
diskMonitor.registerMonitoredDirectory(
cfg.getBackendId() + " backend",
getDirectory(),
cfg.getDiskLowThreshold(),
cfg.getDiskFullThreshold(),
this);
}
@Override
public void removeStorageFiles() throws StorageRuntimeException
{
StorageUtils.removeStorageFiles(backendDirectory);
}
@Override
public StorageStatus getStorageStatus()
{
return storageStatus;
}
@Override
public void diskFullThresholdReached(File directory, long thresholdInBytes) {
storageStatus = statusWhenDiskSpaceFull(directory, thresholdInBytes, config.getBackendId());
}
@Override
public void diskLowThresholdReached(File directory, long thresholdInBytes) {
storageStatus = statusWhenDiskSpaceLow(directory, thresholdInBytes, config.getBackendId());
}
@Override
public void diskSpaceRestored(File directory, long lowThresholdInBytes, long fullThresholdInBytes) {
storageStatus = StorageStatus.working();
}
private static void setData(final DatabaseEntry dbEntry, final ByteSequence bs)
{
dbEntry.setData(bs != null ? bs.toByteArray() : null);
}
private static DatabaseEntry db(final ByteSequence bs)
{
return new DatabaseEntry(bs != null ? bs.toByteArray() : null);
}
private static ByteString valueToBytes(final DatabaseEntry dbValue, boolean isDefined)
{
if (isDefined)
{
return ByteString.wrap(dbValue.getData());
}
return null;
}
}