PersistItStorage.java revision f0a048d41a13eca4cba405da9403c2469ca3d1ea
0N/A/*
0N/A * CDDL HEADER START
0N/A *
0N/A * The contents of this file are subject to the terms of the
0N/A * Common Development and Distribution License, Version 1.0 only
2362N/A * (the "License"). You may not use this file except in compliance
0N/A * with the License.
2362N/A *
0N/A * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
0N/A * or http://forgerock.org/license/CDDLv1.0.html.
0N/A * See the License for the specific language governing permissions
0N/A * and limitations under the License.
0N/A *
0N/A * When distributing Covered Code, include this CDDL HEADER in each
0N/A * file and include the License file at legal-notices/CDDLv1_0.txt.
0N/A * If applicable, add the following below this CDDL HEADER, with the
0N/A * fields enclosed by brackets "[]" replaced with your own identifying
0N/A * information:
0N/A * Portions Copyright [yyyy] [name of copyright owner]
2362N/A *
2362N/A * CDDL HEADER END
2362N/A *
0N/A *
0N/A * Copyright 2014-2015 ForgeRock AS
0N/A */
0N/Apackage org.opends.server.backends.persistit;
0N/A
0N/Aimport static com.persistit.Transaction.CommitPolicy.*;
0N/Aimport static java.util.Arrays.*;
0N/A
0N/Aimport static org.opends.messages.BackendMessages.*;
0N/Aimport static org.opends.messages.ConfigMessages.*;
0N/Aimport static org.opends.messages.UtilityMessages.*;
0N/Aimport static org.opends.server.util.StaticUtils.*;
0N/A
0N/Aimport java.io.File;
0N/Aimport java.io.IOException;
0N/Aimport java.nio.file.Files;
0N/Aimport java.nio.file.Path;
0N/Aimport java.nio.file.Paths;
0N/Aimport java.rmi.RemoteException;
1693N/Aimport java.util.ArrayList;
1693N/Aimport java.util.HashMap;
1693N/Aimport java.util.List;
1693N/Aimport java.util.ListIterator;
5980N/Aimport java.util.Map;
5980N/Aimport java.util.NoSuchElementException;
5980N/Aimport java.util.Queue;
5980N/Aimport java.util.concurrent.ConcurrentLinkedDeque;
5980N/A
5980N/Aimport org.forgerock.i18n.LocalizableMessage;
5980N/Aimport org.forgerock.i18n.slf4j.LocalizedLogger;
0N/Aimport org.forgerock.opendj.config.server.ConfigChangeResult;
0N/Aimport org.forgerock.opendj.config.server.ConfigException;
0N/Aimport org.forgerock.opendj.ldap.ByteSequence;
0N/Aimport org.forgerock.opendj.ldap.ByteString;
0N/Aimport org.opends.server.admin.server.ConfigurationChangeListener;
0N/Aimport org.opends.server.admin.std.server.PersistitBackendCfg;
0N/Aimport org.opends.server.api.Backupable;
0N/Aimport org.opends.server.api.DiskSpaceMonitorHandler;
0N/Aimport org.opends.server.backends.pluggable.spi.Cursor;
0N/Aimport org.opends.server.backends.pluggable.spi.Importer;
0N/Aimport org.opends.server.backends.pluggable.spi.ReadOperation;
0N/Aimport org.opends.server.backends.pluggable.spi.Storage;
0N/Aimport org.opends.server.backends.pluggable.spi.StorageRuntimeException;
1693N/Aimport org.opends.server.backends.pluggable.spi.StorageStatus;
0N/Aimport org.opends.server.backends.pluggable.spi.TreeName;
5980N/Aimport org.opends.server.backends.pluggable.spi.UpdateFunction;
5980N/Aimport org.opends.server.backends.pluggable.spi.WriteOperation;
0N/Aimport org.opends.server.backends.pluggable.spi.WriteableTransaction;
0N/Aimport org.opends.server.core.DirectoryServer;
0N/Aimport org.opends.server.core.MemoryQuota;
0N/Aimport org.opends.server.core.ServerContext;
0N/Aimport org.opends.server.extensions.DiskSpaceMonitor;
0N/Aimport org.opends.server.types.BackupConfig;
0N/Aimport org.opends.server.types.BackupDirectory;
0N/Aimport org.opends.server.types.DirectoryException;
0N/Aimport org.opends.server.types.FilePermission;
0N/Aimport org.opends.server.types.RestoreConfig;
0N/Aimport org.opends.server.util.BackupManager;
0N/A
0N/Aimport com.persistit.Configuration;
0N/Aimport com.persistit.Configuration.BufferPoolConfiguration;
0N/Aimport com.persistit.Exchange;
0N/Aimport com.persistit.Key;
0N/Aimport com.persistit.Persistit;
0N/Aimport com.persistit.Transaction;
0N/Aimport com.persistit.Tree;
0N/Aimport com.persistit.Value;
0N/Aimport com.persistit.Volume;
0N/Aimport com.persistit.VolumeSpecification;
0N/Aimport com.persistit.exception.PersistitException;
0N/Aimport com.persistit.exception.RollbackException;
0N/A
0N/A/** PersistIt database implementation of the {@link Storage} engine. */
0N/A@SuppressWarnings("javadoc")
0N/Apublic final class PersistItStorage implements Storage, Backupable, ConfigurationChangeListener<PersistitBackendCfg>,
0N/A DiskSpaceMonitorHandler
0N/A{
0N/A private static final String VOLUME_NAME = "dj";
0N/A /** The buffer / page size used by the PersistIt storage. */
0N/A private static final int BUFFER_SIZE = 16 * 1024;
0N/A
0N/A /** PersistIt implementation of the {@link Cursor} interface. */
0N/A private final class CursorImpl implements Cursor<ByteString, ByteString>
0N/A {
0N/A private ByteString currentKey;
0N/A private ByteString currentValue;
0N/A private final Exchange exchange;
0N/A
0N/A private CursorImpl(final Exchange exchange)
0N/A {
0N/A this.exchange = exchange;
0N/A }
0N/A
0N/A @Override
0N/A public void close()
0N/A {
0N/A // Release immediately because this exchange did not come from the txn cache
0N/A db.releaseExchange(exchange);
0N/A }
0N/A
0N/A @Override
0N/A public boolean isDefined() {
0N/A return exchange.getValue().isDefined();
0N/A }
0N/A
0N/A @Override
0N/A public ByteString getKey()
0N/A {
0N/A if (currentKey == null)
0N/A {
0N/A throwIfUndefined();
0N/A currentKey = ByteString.wrap(exchange.getKey().reset().decodeByteArray());
0N/A }
0N/A return currentKey;
0N/A }
0N/A
0N/A @Override
0N/A public ByteString getValue()
0N/A {
0N/A if (currentValue == null)
0N/A {
0N/A throwIfUndefined();
0N/A currentValue = ByteString.wrap(exchange.getValue().getByteArray());
0N/A }
0N/A return currentValue;
0N/A }
0N/A
0N/A @Override
0N/A public boolean next()
0N/A {
0N/A clearCurrentKeyAndValue();
0N/A try
0N/A {
0N/A return exchange.next();
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public boolean positionToKey(final ByteSequence key)
0N/A {
0N/A clearCurrentKeyAndValue();
0N/A bytesToKey(exchange.getKey(), key);
0N/A try
0N/A {
0N/A exchange.fetch();
0N/A return exchange.getValue().isDefined();
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public boolean positionToKeyOrNext(final ByteSequence key)
0N/A {
0N/A clearCurrentKeyAndValue();
0N/A bytesToKey(exchange.getKey(), key);
0N/A try
0N/A {
0N/A exchange.fetch();
0N/A return exchange.getValue().isDefined() || exchange.next();
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public boolean positionToIndex(int index)
0N/A {
0N/A // There doesn't seem to be a way to optimize this using Persistit.
0N/A clearCurrentKeyAndValue();
0N/A exchange.getKey().to(Key.BEFORE);
0N/A try
0N/A {
0N/A for (int i = 0; i <= index; i++)
0N/A {
0N/A if (!exchange.next())
0N/A {
0N/A return false;
0N/A }
0N/A }
0N/A return true;
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public boolean positionToLastKey()
0N/A {
0N/A clearCurrentKeyAndValue();
0N/A exchange.getKey().to(Key.AFTER);
0N/A try
0N/A {
0N/A return exchange.previous();
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A private void clearCurrentKeyAndValue()
0N/A {
0N/A currentKey = null;
0N/A currentValue = null;
0N/A }
0N/A
0N/A private void throwIfUndefined() {
0N/A if (!isDefined()) {
0N/A throw new NoSuchElementException();
0N/A }
0N/A }
0N/A }
0N/A
0N/A /** PersistIt implementation of the {@link Importer} interface. */
0N/A private final class ImporterImpl implements Importer
0N/A {
0N/A private final Map<TreeName, Tree> trees = new HashMap<TreeName, Tree>();
0N/A private final Queue<Map<TreeName, Exchange>> allExchanges = new ConcurrentLinkedDeque<>();
0N/A private final ThreadLocal<Map<TreeName, Exchange>> exchanges = new ThreadLocal<Map<TreeName, Exchange>>()
0N/A {
0N/A @Override
0N/A protected Map<TreeName, Exchange> initialValue()
0N/A {
0N/A final Map<TreeName, Exchange> value = new HashMap<>();
0N/A allExchanges.add(value);
0N/A return value;
0N/A }
0N/A };
0N/A
0N/A @Override
0N/A public void close()
0N/A {
0N/A for (Map<TreeName, Exchange> map : allExchanges)
0N/A {
0N/A for (Exchange exchange : map.values())
0N/A {
0N/A db.releaseExchange(exchange);
0N/A }
0N/A map.clear();
0N/A }
0N/A PersistItStorage.this.close();
0N/A }
0N/A
0N/A @Override
0N/A public void createTree(final TreeName treeName)
0N/A {
0N/A try
0N/A {
0N/A final Tree tree = volume.getTree(mangleTreeName(treeName), true);
0N/A trees.put(treeName, tree);
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value)
0N/A {
0N/A try
0N/A {
0N/A final Exchange ex = getExchangeFromCache(treeName);
0N/A bytesToKey(ex.getKey(), key);
0N/A bytesToValue(ex.getValue(), value);
0N/A ex.store();
0N/A }
0N/A catch (final Exception e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public boolean delete(final TreeName treeName, final ByteSequence key)
0N/A {
0N/A try
0N/A {
0N/A final Exchange ex = getExchangeFromCache(treeName);
0N/A bytesToKey(ex.getKey(), key);
0N/A return ex.remove();
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public ByteString read(final TreeName treeName, final ByteSequence key)
0N/A {
0N/A try
0N/A {
0N/A final Exchange ex = getExchangeFromCache(treeName);
0N/A bytesToKey(ex.getKey(), key);
0N/A ex.fetch();
0N/A return valueToBytes(ex.getValue());
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A private Exchange getExchangeFromCache(final TreeName treeName) throws PersistitException
0N/A {
0N/A Map<TreeName, Exchange> threadExchanges = exchanges.get();
0N/A Exchange exchange = threadExchanges.get(treeName);
0N/A if (exchange == null)
0N/A {
0N/A exchange = getNewExchange(treeName, false);
0N/A threadExchanges.put(treeName, exchange);
0N/A }
0N/A return exchange;
0N/A }
0N/A }
0N/A
0N/A /** PersistIt implementation of the {@link WriteableTransaction} interface. */
0N/A private final class StorageImpl implements WriteableTransaction
0N/A {
0N/A private final Map<TreeName, Exchange> exchanges = new HashMap<TreeName, Exchange>();
0N/A
0N/A @Override
0N/A public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value)
0N/A {
0N/A try
0N/A {
0N/A final Exchange ex = getExchangeFromCache(treeName);
0N/A bytesToKey(ex.getKey(), key);
0N/A bytesToValue(ex.getValue(), value);
0N/A ex.store();
0N/A }
0N/A catch (final Exception e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public boolean delete(final TreeName treeName, final ByteSequence key)
0N/A {
0N/A try
0N/A {
0N/A final Exchange ex = getExchangeFromCache(treeName);
0N/A bytesToKey(ex.getKey(), key);
0N/A return ex.remove();
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public void deleteTree(final TreeName treeName)
0N/A {
0N/A Exchange ex = null;
0N/A try
0N/A {
0N/A ex = getExchangeFromCache(treeName);
0N/A ex.removeTree();
0N/A }
0N/A catch (final PersistitException e)
0N/A {
0N/A throw new StorageRuntimeException(e);
0N/A }
0N/A finally
0N/A {
0N/A exchanges.values().remove(ex);
0N/A db.releaseExchange(ex);
0N/A }
0N/A }
0N/A
0N/A @Override
0N/A public long getRecordCount(TreeName treeName)
0N/A {
0N/A // FIXME: is there a better/quicker way to do this?
0N/A final Cursor<?, ?> cursor = openCursor(treeName);
0N/A try
0N/A {
0N/A long count = 0;
0N/A while (cursor.next())
0N/A {
0N/A count++;
0N/A }
0N/A return count;
0N/A }
0N/A finally
0N/A {
0N/A cursor.close();
0N/A }
0N/A }
1693N/A
1693N/A @Override
public Cursor<ByteString, ByteString> openCursor(final TreeName treeName)
{
try
{
/*
* Acquire a new exchange for the cursor rather than using a cached
* exchange in order to avoid reentrant accesses to the same tree
* interfering with the cursor position.
*/
return new CursorImpl(getNewExchange(treeName, false));
}
catch (final PersistitException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public void openTree(final TreeName treeName)
{
Exchange ex = null;
try
{
ex = getNewExchange(treeName, true);
}
catch (final PersistitException e)
{
throw new StorageRuntimeException(e);
}
finally
{
db.releaseExchange(ex);
}
}
@Override
public ByteString read(final TreeName treeName, final ByteSequence key)
{
try
{
final Exchange ex = getExchangeFromCache(treeName);
bytesToKey(ex.getKey(), key);
ex.fetch();
return valueToBytes(ex.getValue());
}
catch (final PersistitException e)
{
throw new StorageRuntimeException(e);
}
}
@Override
public void renameTree(final TreeName oldTreeName, final TreeName newTreeName)
{
throw new UnsupportedOperationException();
}
@Override
public boolean update(final TreeName treeName, final ByteSequence key, final UpdateFunction f)
{
try
{
final Exchange ex = getExchangeFromCache(treeName);
bytesToKey(ex.getKey(), key);
ex.fetch();
final ByteSequence oldValue = valueToBytes(ex.getValue());
final ByteSequence newValue = f.computeNewValue(oldValue);
if (!equals(newValue, oldValue))
{
if (newValue == null)
{
ex.remove();
}
else
{
ex.getValue().clear().putByteArray(newValue.toByteArray());
ex.store();
}
return true;
}
return false;
}
catch (final Exception e)
{
throw new StorageRuntimeException(e);
}
}
private boolean equals(ByteSequence b1, ByteSequence b2)
{
if (b1 == null)
{
return b2 == null;
}
return b1.equals(b2);
}
private Exchange getExchangeFromCache(final TreeName treeName) throws PersistitException
{
Exchange exchange = exchanges.get(treeName);
if (exchange == null)
{
exchange = getNewExchange(treeName, false);
exchanges.put(treeName, exchange);
}
return exchange;
}
private void release()
{
for (final Exchange ex : exchanges.values())
{
db.releaseExchange(ex);
}
exchanges.clear();
}
}
private Exchange getNewExchange(final TreeName treeName, final boolean create) throws PersistitException
{
return db.getExchange(volume, mangleTreeName(treeName), create);
}
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
private final ServerContext serverContext;
private File backendDirectory;
private Persistit db;
private Volume volume;
private PersistitBackendCfg config;
private DiskSpaceMonitor diskMonitor;
private MemoryQuota memQuota;
private StorageStatus storageStatus = StorageStatus.working();
/**
* Creates a new persistit storage with the provided configuration.
*
* @param cfg
* The configuration.
* @param serverContext
* This server instance context
* @throws ConfigException if memory cannot be reserved
*/
// FIXME: should be package private once importer is decoupled.
public PersistItStorage(final PersistitBackendCfg cfg, ServerContext serverContext) throws ConfigException
{
this.serverContext = serverContext;
backendDirectory = new File(getFileForPath(cfg.getDBDirectory()), cfg.getBackendId());
config = cfg;
cfg.addPersistitChangeListener(this);
}
private Configuration buildConfiguration()
{
final Configuration dbCfg = new Configuration();
dbCfg.setLogFile(new File(backendDirectory, VOLUME_NAME + ".log").getPath());
dbCfg.setJournalPath(new File(backendDirectory, VOLUME_NAME + "_journal").getPath());
dbCfg.setVolumeList(asList(new VolumeSpecification(new File(backendDirectory, VOLUME_NAME).getPath(), null,
BUFFER_SIZE, 4096, Long.MAX_VALUE / BUFFER_SIZE, 2048, true, false, false)));
final BufferPoolConfiguration bufferPoolCfg = getBufferPoolCfg(dbCfg);
bufferPoolCfg.setMaximumCount(Integer.MAX_VALUE);
diskMonitor = serverContext.getDiskSpaceMonitor();
memQuota = serverContext.getMemoryQuota();
if (config.getDBCacheSize() > 0)
{
bufferPoolCfg.setMaximumMemory(config.getDBCacheSize());
memQuota.acquireMemory(config.getDBCacheSize());
}
else
{
bufferPoolCfg.setFraction(config.getDBCachePercent() / 100.0f);
memQuota.acquireMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
}
dbCfg.setCommitPolicy(config.isDBTxnNoSync() ? SOFT : GROUP);
return dbCfg;
}
/** {@inheritDoc} */
@Override
public void close()
{
if (db != null)
{
try
{
db.close();
db = null;
}
catch (final PersistitException e)
{
throw new IllegalStateException(e);
}
}
if (config.getDBCacheSize() > 0)
{
memQuota.releaseMemory(config.getDBCacheSize());
}
else
{
memQuota.releaseMemory(memQuota.memPercentToBytes(config.getDBCachePercent()));
}
config.removePersistitChangeListener(this);
diskMonitor.deregisterMonitoredDirectory(getDirectory(), this);
}
private static BufferPoolConfiguration getBufferPoolCfg(Configuration dbCfg)
{
return dbCfg.getBufferPoolMap().get(BUFFER_SIZE);
}
/** {@inheritDoc} */
@Override
public void open() throws ConfigException, StorageRuntimeException
{
open0(buildConfiguration());
}
private void open0(final Configuration dbCfg) throws ConfigException
{
setupStorageFiles();
try
{
if (db != null)
{
throw new IllegalStateException(
"Database is already open, either the backend is enabled or an import is currently running.");
}
db = new Persistit(dbCfg);
final long bufferCount = getBufferPoolCfg(dbCfg).computeBufferCount(db.getAvailableHeap());
final long totalSize = bufferCount * BUFFER_SIZE / 1024;
logger.info(NOTE_PERSISTIT_MEMORY_CFG, config.getBackendId(), bufferCount, BUFFER_SIZE, totalSize);
db.initialize();
volume = db.loadVolume(VOLUME_NAME);
}
catch (final PersistitException e)
{
throw new StorageRuntimeException(e);
}
diskMonitor.registerMonitoredDirectory(
config.getBackendId() + " backend",
getDirectory(),
config.getDiskLowThreshold(),
config.getDiskFullThreshold(),
this);
}
/** {@inheritDoc} */
@Override
public <T> T read(final ReadOperation<T> operation) throws Exception
{
final Transaction txn = db.getTransaction();
for (;;)
{
txn.begin();
try
{
final StorageImpl storageImpl = new StorageImpl();
try
{
final T result = operation.run(storageImpl);
txn.commit();
return result;
}
catch (final StorageRuntimeException e)
{
if (e.getCause() != null)
{
throw (Exception) e.getCause();
}
throw e;
}
finally
{
storageImpl.release();
}
}
catch (final RollbackException e)
{
// retry
}
catch (final Exception e)
{
txn.rollback();
throw e;
}
finally
{
txn.end();
}
}
}
/** {@inheritDoc} */
@Override
public Importer startImport() throws ConfigException, StorageRuntimeException
{
open0(buildConfiguration());
return new ImporterImpl();
}
private static String mangleTreeName(final TreeName treeName)
{
StringBuilder mangled = new StringBuilder();
String name = treeName.toString();
for (int idx = 0; idx < name.length(); idx++)
{
char ch = name.charAt(idx);
if (ch == '=' || ch == ',')
{
ch = '_';
}
mangled.append(ch);
}
return mangled.toString();
}
/** {@inheritDoc} */
@Override
public void write(final WriteOperation operation) throws Exception
{
final Transaction txn = db.getTransaction();
for (;;)
{
txn.begin();
try
{
final StorageImpl storageImpl = new StorageImpl();
try
{
operation.run(storageImpl);
txn.commit();
return;
}
catch (final StorageRuntimeException e)
{
if (e.getCause() != null)
{
throw (Exception) e.getCause();
}
throw e;
}
finally
{
storageImpl.release();
}
}
catch (final RollbackException e)
{
// retry
}
catch (final Exception e)
{
txn.rollback();
throw e;
}
finally
{
txn.end();
}
}
}
@Override
public boolean supportsBackupAndRestore()
{
return true;
}
@Override
public File getDirectory()
{
File parentDir = getFileForPath(config.getDBDirectory());
return new File(parentDir, config.getBackendId());
}
@Override
public ListIterator<Path> getFilesToBackup() throws DirectoryException
{
try
{
// FIXME: use full programmatic way of retrieving backup file once available in persistIt
String filesAsString = db.getManagement().execute("backup -f");
String[] allFiles = filesAsString.split("[\r\n]+");
final List<Path> files = new ArrayList<>();
for (String file : allFiles)
{
files.add(Paths.get(file));
}
return files.listIterator();
}
catch (RemoteException e)
{
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
ERR_BACKEND_LIST_FILES_TO_BACKUP.get(config.getBackendId(), stackTraceToSingleLineString(e)));
}
}
@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);
}
}
/**
* Switch the database in append only mode.
* <p>
* This is a mandatory operation before performing a backup.
*/
private void switchToAppendOnlyMode() throws DirectoryException
{
try
{
// FIXME: use full programmatic way of switching to this mode once available in persistIt
db.getManagement().execute("backup -y -a -c");
}
catch (RemoteException e)
{
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
ERR_BACKEND_SWITCH_TO_APPEND_MODE.get(config.getBackendId(), stackTraceToSingleLineString(e)));
}
}
/**
* Terminate the append only mode of the database.
* <p>
* This should be called only when database was previously switched to append only mode.
*/
private void endAppendOnlyMode() throws DirectoryException
{
try
{
// FIXME: use full programmatic way of ending append mode once available in persistIt
db.getManagement().execute("backup -e");
}
catch (RemoteException e)
{
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
ERR_BACKEND_END_APPEND_MODE.get(config.getBackendId(), stackTraceToSingleLineString(e)));
}
}
@Override
public void createBackup(BackupConfig backupConfig) throws DirectoryException
{
switchToAppendOnlyMode();
try
{
new BackupManager(config.getBackendId()).createBackup(this, backupConfig);
}
finally
{
endAppendOnlyMode();
}
}
@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);
}
/**
* TODO: it would be nice to use the low-level key/value APIs. They seem quite
* inefficient at the moment for simple byte arrays.
*/
private static Key bytesToKey(final Key key, final ByteSequence bytes)
{
final byte[] tmp = bytes.toByteArray();
return key.clear().appendByteArray(tmp, 0, tmp.length);
}
private static Value bytesToValue(final Value value, final ByteSequence bytes)
{
value.clear().putByteArray(bytes.toByteArray());
return value;
}
private static ByteString valueToBytes(final Value value)
{
if (value.isDefined())
{
return ByteString.wrap(value.getByteArray());
}
return null;
}
/** {@inheritDoc} */
@Override
public boolean isConfigurationChangeAcceptable(PersistitBackendCfg 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(PersistitBackendCfg 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 TODO
* @return true if newly created backend has a valid configuration
*/
public static boolean isConfigurationAcceptable(PersistitBackendCfg 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(PersistitBackendCfg cfg,
List<LocalizableMessage> unacceptableReasons)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
File parentDirectory = getFileForPath(cfg.getDBDirectory());
File newBackendDirectory = new File(parentDirectory, cfg.getBackendId());
checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, true);
checkDBDirPermissions(cfg, ccr);
if (!ccr.getMessages().isEmpty())
{
unacceptableReasons.addAll(ccr.getMessages());
return false;
}
return true;
}
/**
* Checks a directory exists or can actually be created.
*
* @param backendDir the directory to check for
* @param ccr the list of reasons to return upstream or null if called from setupStorage()
* @param cleanup true if the directory should be deleted after creation
*/
private static void checkDBDirExistsOrCanCreate(File backendDir, ConfigChangeResult ccr, boolean cleanup)
{
if (!backendDir.exists())
{
if(!backendDir.mkdirs())
{
addErrorMessage(ccr, ERR_CREATE_FAIL.get(backendDir.getPath()));
}
if (cleanup)
{
backendDir.delete();
}
}
else if (!backendDir.isDirectory())
{
addErrorMessage(ccr, ERR_DIRECTORY_INVALID.get(backendDir.getPath()));
}
}
/**
* Returns false if directory permissions in the configuration are invalid. Otherwise returns the
* same value as it was passed in.
*
* @param cfg a (possibly new) backend configuration
* @param ccr the current list of change results
* @throws forwards a file exception
*/
private static void checkDBDirPermissions(PersistitBackendCfg cfg, ConfigChangeResult ccr)
{
try
{
FilePermission backendPermission = decodeDBDirPermissions(cfg);
// Make sure the mode will allow the server itself access to the database
if(!backendPermission.isOwnerWritable() ||
!backendPermission.isOwnerReadable() ||
!backendPermission.isOwnerExecutable())
{
addErrorMessage(ccr, ERR_CONFIG_BACKEND_INSANE_MODE.get(cfg.getDBDirectoryPermissions()));
}
}
catch(ConfigException ce)
{
addErrorMessage(ccr, ce.getMessageObject());
}
}
/**
* Sets files permissions on the backend directory.
*
* @param backendDir the directory to setup
* @param curCfg a backend configuration
*/
private void setDBDirPermissions(PersistitBackendCfg curCfg, File backendDir) throws ConfigException
{
FilePermission backendPermission = decodeDBDirPermissions(curCfg);
// Get the backend database backendDirectory permissions and apply
try
{
if(!FilePermission.setPermissions(backendDir, backendPermission))
{
logger.warn(WARN_UNABLE_SET_PERMISSIONS, backendPermission, backendDir);
}
}
catch(Exception e)
{
// Log an warning that the permissions were not set.
logger.warn(WARN_SET_PERMISSIONS_FAILED, backendDir, e);
}
}
private static FilePermission decodeDBDirPermissions(PersistitBackendCfg curCfg) throws ConfigException
{
try
{
return FilePermission.decodeUNIXMode(curCfg.getDBDirectoryPermissions());
}
catch (Exception e)
{
throw new ConfigException(ERR_CONFIG_BACKEND_MODE_INVALID.get(curCfg.dn()));
}
}
/** {@inheritDoc} */
@Override
public ConfigChangeResult applyConfigurationChange(PersistitBackendCfg cfg)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
try
{
File parentDirectory = getFileForPath(cfg.getDBDirectory());
File newBackendDirectory = new File(parentDirectory, cfg.getBackendId());
// 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, ccr);
if (!ccr.getMessages().isEmpty())
{
return ccr;
}
setDBDirPermissions(cfg, newBackendDirectory);
}
diskMonitor.registerMonitoredDirectory(
config.getBackendId() + " backend",
getDirectory(),
cfg.getDiskLowThreshold(),
cfg.getDiskFullThreshold(),
this);
config = cfg;
}
catch (Exception e)
{
addErrorMessage(ccr, LocalizableMessage.raw(stackTraceToSingleLineString(e)));
}
return ccr;
}
private static void addErrorMessage(final ConfigChangeResult ccr, LocalizableMessage message)
{
ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
ccr.addMessage(message);
}
private void setupStorageFiles() throws ConfigException
{
ConfigChangeResult ccr = new ConfigChangeResult();
checkDBDirExistsOrCanCreate(backendDirectory, ccr, false);
if (!ccr.getMessages().isEmpty())
{
throw new ConfigException(ccr.getMessages().get(0));
}
checkDBDirPermissions(config, ccr);
if (!ccr.getMessages().isEmpty())
{
throw new ConfigException(ccr.getMessages().get(0));
}
setDBDirPermissions(config, backendDirectory);
}
@Override
public void removeStorageFiles() throws StorageRuntimeException
{
if (!backendDirectory.exists())
{
return;
}
if (!backendDirectory.isDirectory())
{
throw new StorageRuntimeException(ERR_DIRECTORY_INVALID.get(backendDirectory.getPath()).toString());
}
try
{
File[] files = backendDirectory.listFiles();
for (File f : files)
{
f.delete();
}
}
catch (Exception e)
{
logger.traceException(e);
throw new StorageRuntimeException(ERR_REMOVE_FAIL.get(e.getMessage()).toString(), e);
}
}
@Override
public StorageStatus getStorageStatus()
{
return storageStatus;
}
/** {@inheritDoc} */
@Override
public void diskFullThresholdReached(File directory, long thresholdInBytes) {
storageStatus = StorageStatus.unusable(
WARN_DISK_SPACE_FULL_THRESHOLD_CROSSED.get(directory.getFreeSpace(), directory.getAbsolutePath(),
thresholdInBytes, config.getBackendId()));
}
/** {@inheritDoc} */
@Override
public void diskLowThresholdReached(File directory, long thresholdInBytes) {
storageStatus = StorageStatus.lockedDown(
WARN_DISK_SPACE_LOW_THRESHOLD_CROSSED.get(directory.getFreeSpace(), directory.getAbsolutePath(),
thresholdInBytes, config.getBackendId()));
}
/** {@inheritDoc} */
@Override
public void diskSpaceRestored(File directory, long lowThresholdInBytes, long fullThresholdInBytes) {
storageStatus = StorageStatus.working();
}
}