package io.eguan.ibs;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import io.eguan.configuration.MetaConfiguration;
import io.eguan.utils.ByteArrays;
import io.eguan.utils.Files;
import io.eguan.utils.Files.HandledFile;
import io.eguan.utils.Files.OpenedFileHandler;
import io.eguan.utils.mapper.FileMapper;
import io.eguan.utils.mapper.FileMapperConfigurationContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.concurrent.GuardedBy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.protobuf.ByteString;
/**
* Stores blocks in files. The transactions are stored in memory.
*
* @author oodrive
* @author llambert
*
*/
final class IbsFilesDB extends IbsDBAbstract {
private static final Logger LOGGER = LoggerFactory.getLogger(IbsFilesDB.class);
private static final class IbsHandledFile extends HandledFile<String> {
/** Associated file */
private final File file;
/** Id of the file */
private final String id;
/** Set when the file is opened */
private final Lock openedLock = new ReentrantLock();
/** Set when the file is opened */
@GuardedBy(value = "openedLock")
private FileChannel channel;
private boolean readOnly;
private boolean newFile;
IbsHandledFile(final File file, final String id, final boolean newFile) {
super();
this.file = file;
this.id = id;
this.newFile = newFile;
}
final boolean isNewFile() {
return newFile;
}
final void setNewFile(final boolean newFile) {
this.newFile = newFile;
}
@Override
protected final void open(final boolean readOnly) throws IOException, IllegalStateException {
openedLock.lock();
try {
if (channel != null) {
throw new IllegalStateException("opened");
}
this.readOnly = readOnly;
if (readOnly) {
channel = FileChannel.open(file.toPath(), StandardOpenOption.READ);
}
else {
channel = FileChannel.open(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE);
}
}
finally {
openedLock.unlock();
}
}
@Override
protected final boolean isOpened() {
openedLock.lock();
try {
return channel != null;
}
finally {
openedLock.unlock();
}
}
@Override
protected final boolean isOpenedLock() {
openedLock.lock();
try {
// No read/write in progress under openedLock
return false;
}
finally {
openedLock.unlock();
}
}
@Override
protected final boolean isOpenedReadOnly() {
openedLock.lock();
try {
return channel != null && readOnly;
}
finally {
openedLock.unlock();
}
}
@Override
protected final void close() {
openedLock.lock();
try {
if (channel != null) {
try {
channel.close();
}
catch (final Throwable t) {
LOGGER.warn("Failed to close '" + file.getAbsolutePath() + "'", t);
}
channel = null;
}
}
finally {
openedLock.unlock();
}
}
@Override
protected final String getId() {
return id;
}
final int read(final ByteBuffer buffer) throws IOException {
openedLock.lock();
try {
final long size = channel.size();
if (buffer.remaining() < size) {
throw new IbsBufferTooSmallException((int) size);
}
channel.position(0);
return channel.read(buffer);
}
finally {
openedLock.unlock();
}
}
/**
* Writes only if the file is empty (does not override a key/pair association).
*
* @param buffer
* @throws IOException
*/
final void write(final ByteBuffer buffer) throws IOException {
openedLock.lock();
try {
if (channel.size() == 0) {
channel.truncate(buffer.remaining()).position(0);
channel.write(buffer);
}
}
finally {
openedLock.unlock();
}
}
/**
* Deletes the file.
*/
final void delete() {
file.delete();
}
}
/** Configuration for a <byte1>/<byte2>/<file name> mapping */
private static final MetaConfiguration FILE_MAPPING_CONFIGURATION;
static {
try {
// Write config
final ByteArrayOutputStream baos = new ByteArrayOutputStream(10);
try (PrintStream config = new PrintStream(baos)) {
config.println("io.eguan.filemapping.dir.structure.depth=2");
config.println("io.eguan.filemapping.dir.prefix.length=3");
}
finally {
baos.close();
}
// Create config
try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())) {
FILE_MAPPING_CONFIGURATION = MetaConfiguration.newConfiguration(bais,
FileMapperConfigurationContext.getInstance());
}
}
catch (final Exception e) {
throw new IllegalStateException("Failed to create configuration", e);
}
}
/** File mapper to find files containing the blocks */
private final FileMapper fileMapper;
/** Transaction handling */
private final IbsMemTransaction ibsMemTransaction;
/** Handler of opened Ibs files */
private OpenedFileHandler<IbsHandledFile, String> openedFileHandler;
/**
* Create a new instance.
*
* @param ibsPath
* directory containing the files.
*/
private IbsFilesDB(final String ibsPath, final File ibsDir) {
super(ibsPath);
fileMapper = FileMapper.Type.DEEP.newInstance(ibsDir, 9, FILE_MAPPING_CONFIGURATION);
ibsMemTransaction = new IbsMemTransaction(this);
}
static final Ibs createIbs(final File ibsPath) throws IbsException {
final File ibsDir = checkIbsPath(ibsPath);
if (ibsDir.list().length != 0) {
throw new IbsException(ibsDir.getAbsolutePath(), IbsErrorCode.CREATE_IN_NON_EMPTY_DIR);
}
// Add an empty marker file
try {
if (!new File(ibsDir, "created").createNewFile()) {
// Failed to create a new file: already exist?
throw new IbsException(ibsDir.getAbsolutePath(), IbsErrorCode.CREATE_IN_NON_EMPTY_DIR);
}
}
catch (final IOException e) {
// Failed to create a new file: access denied?
throw new IbsException(ibsDir.getAbsolutePath(), e);
}
return new IbsFilesDB(ibsDir.getAbsolutePath(), ibsDir);
}
/**
* Opens an existing Ibs.
*
* @param ibsPath
* @return the new opened Ibs
* @throws IbsException
*/
static final Ibs openIbs(final File ibsPath) throws IbsException {
final File ibsDir = checkIbsPath(ibsPath);
if (ibsDir.list().length == 0) {
throw new IbsException(ibsDir.getAbsolutePath(), IbsErrorCode.INIT_FROM_EMPTY_DIR);
}
return new IbsFilesDB(ibsDir.getAbsolutePath(), ibsDir);
}
private static final File checkIbsPath(final File ibsPath) {
// Make sure the path is a directory
if (ibsPath.isDirectory()) {
// Is a directory
return ibsPath;
}
// The path may denote a IbsLevelDB configuration file
final File ibpPath = selectDirectoryFromFile(ibsPath);
if (ibpPath != null && ibpPath.isDirectory()) {
return ibpPath;
}
// Nothing found
throw new IbsException(ibsPath + " is not a directory", IbsErrorCode.INVALID_IBS_ID);
}
private static final String IBP_PATH_STR = "ibp_path";
private static final String IBP_PATH_DELIM = ",";
/**
* Select a directory from the configuration of a {@link IbsLevelDB}. Take the first directory declared in
* <code>ibp_path</code>.
*
* @param ibsFile
* {@link IbsLevelDB} configuration file
* @return the selected directory or <code>null</code> if the <code>ibsFile</code> is not a valid configuration file
*/
private static final File selectDirectoryFromFile(final File ibsFile) {
try {
// Try to load the configuration
final Properties config = new Properties();
try (FileInputStream fis = new FileInputStream(ibsFile)) {
config.load(fis);
}
final String ibpList = config.getProperty(IBP_PATH_STR);
if (ibpList != null) {
final String ibp = new StringTokenizer(ibpList, IBP_PATH_DELIM).nextToken();
return new File(ibp);
}
// No IBP defined
return null;
}
catch (final Exception e) {
LOGGER.warn("'" + ibsFile.getAbsolutePath() + "' is not a valid configuration file");
return null;
}
}
@Override
public final boolean isHotDataEnabled() throws IbsException {
// No replace yet
return false;
}
@Override
public final int get(final byte[] key, final ByteBuffer data, final int offset, final int length)
throws IbsException, IbsIOException, IbsBufferTooSmallException, IllegalArgumentException,
IndexOutOfBoundsException, NullPointerException {
// TODO shared access to the IBS state during the whole get?
if (!started || closed) {
throw new IbsException(toString());
}
checkArgs(key, data, offset, length);
final IbsHandledFile file = getFile(key, false);
if (file == null) {
throw new IbsIOException(toString() + ": " + IbsErrorCode.NOT_FOUND, IbsErrorCode.NOT_FOUND);
}
try {
openedFileHandler.open(file, true);
try {
final int prevPosition = data.position();
try {
data.position(offset);
data.limit(offset + length);
return file.read(data);
}
finally {
data.rewind().position(prevPosition);
}
}
finally {
openedFileHandler.unlock(file);
}
}
catch (final IbsIOException e) {
throw e;
}
catch (final Exception e) {
throw new IbsIOException(ibsPath + ": fail to read file '" + file.getId() + "'", IbsErrorCode.IO_ERROR, e);
}
}
@Override
public final void del(final byte[] key) throws IbsException, IbsIOException, NullPointerException {
// TODO shared access to the IBS state during the whole get?
if (!started || closed) {
throw new IbsException(toString());
}
// Delete the file and remove it from openedFileHandler
final IbsHandledFile file = getFile(key, true);
try {
openedFileHandler.flush(file);
file.delete();
}
finally {
openedFileHandler.cacheRemove(file.getId());
}
}
@Override
public final boolean put(final byte[] key, final ByteBuffer data) throws IbsException, IbsIOException,
NullPointerException {
if (!started || closed) {
throw new IbsException(toString());
}
if (data == null) {
if (getFile(key, false) == null) {
throw new IbsIOException(toString() + ": " + IbsErrorCode.NOT_FOUND, IbsErrorCode.NOT_FOUND);
}
else {
return false;
}
}
return put(key, data, data.position(), data.remaining());
}
@Override
public final boolean put(final byte[] key, final ByteString data) throws IbsException, IbsIOException,
NullPointerException {
return put(key, data.asReadOnlyByteBuffer());
}
@Override
public final boolean replace(final byte[] oldKey, final byte[] newKey, final ByteBuffer data) throws IbsException,
IbsIOException {
// No replace yet
Objects.requireNonNull(oldKey);
return put(newKey, data);
}
@Override
public final int createTransaction() throws IbsException, IllegalArgumentException, IbsIOException {
if (!started || closed) {
throw new IbsException(toString());
}
return ibsMemTransaction.createTransaction();
}
@Override
public final void commit(final int txId) throws IbsException, IllegalArgumentException, IbsIOException {
if (!started || closed) {
throw new IbsException(toString());
}
if (txId <= 0) {
throw new IllegalArgumentException("txId=" + txId);
}
ibsMemTransaction.commit(txId);
}
@Override
public final void rollback(final int txId) throws IbsException, IllegalArgumentException, IbsIOException {
if (!started || closed) {
throw new IbsException(toString());
}
if (txId <= 0) {
throw new IllegalArgumentException("txId=" + txId);
}
ibsMemTransaction.rollback(txId);
}
@Override
protected final boolean doPut(final int txId, final byte[] key, final ByteBuffer data, final int offset,
final int length) throws IbsException, IbsIOException, IllegalArgumentException, IndexOutOfBoundsException,
NullPointerException {
// TODO shared access to the IBS state during the whole put?
if (!started || closed) {
throw new IbsException(toString());
}
checkArgs(key, data, offset, length);
// Transaction?
if (txId > 0) {
// Check if it's a new key
final boolean newKey = getFile(key, false) == null;
ibsMemTransaction.put(txId, key, data, offset, length);
return newKey;
}
final IbsHandledFile file = getFile(key, true);
try {
openedFileHandler.open(file, false);
try {
file.write(data);
}
finally {
openedFileHandler.unlock(file);
}
}
catch (final Exception e) {
throw new IbsIOException(ibsPath + ": fail to write file '" + file.getId() + "'", IbsErrorCode.IO_ERROR, e);
}
return file.isNewFile();
}
@Override
protected final boolean doReplace(final int txId, final byte[] oldKey, final byte[] newKey, final ByteBuffer data,
final int offset, final int length) throws IbsException, IllegalArgumentException, IbsIOException,
IndexOutOfBoundsException, NullPointerException {
// Check references
Objects.requireNonNull(oldKey);
// No replace yet
return doPut(txId, newKey, data, offset, length);
}
@Override
protected final int doStart() {
openedFileHandler = Files.newOpenedFileHandler();
return 0;
}
@Override
protected final int doStop() {
// Close opened files
try {
openedFileHandler.cancel();
}
catch (final Throwable t) {
LOGGER.warn("Error while cancelling task", t);
}
try {
openedFileHandler.closeAll();
}
catch (final Throwable t) {
LOGGER.warn("Error while closing Ibs files", t);
}
openedFileHandler = null;
// Clear pending transactions
ibsMemTransaction.clear();
return 0;
}
@Override
protected final int doClose() {
return 0;
}
@Override
protected final int doDestroy() {
try {
final File ibsDir = new File(ibsPath);
Files.deleteRecursive(ibsDir.toPath());
}
catch (final IOException e) {
LOGGER.warn("Failed to delete '" + ibsPath + "'", e);
}
return 0;
}
/**
* @param key
* @param create
* @return the file created or found or <code>null</code> if <code>create</code> is <code>false</code>
* @throws IbsIOException
*/
private final IbsHandledFile getFile(final byte[] key, final boolean create) throws IbsIOException {
final String id = ByteArrays.toHex(key);
final IbsHandledFile ibsHandledFile;
try {
final Lock fileInstancesLock = openedFileHandler.getCacheWriteLock();
fileInstancesLock.lock();
try {
// Already loaded?
final IbsHandledFile ibsHandledFileTmp = openedFileHandler.cacheLookup(id);
if (ibsHandledFileTmp != null) {
ibsHandledFileTmp.setNewFile(false);
return ibsHandledFileTmp;
}
final File file = fileMapper.mapIdToFile(id);
// Create parents and the file if needed
file.getParentFile().mkdirs();
if (!create && !file.exists()) {
return null;
}
final boolean newFile = file.createNewFile();
ibsHandledFile = new IbsHandledFile(file, id, newFile);
openedFileHandler.cachePut(id, ibsHandledFile);
}
finally {
fileInstancesLock.unlock();
}
return ibsHandledFile;
}
catch (final Exception e) {
throw new IbsIOException(ibsPath + ": fail to get file '" + id + "'", IbsErrorCode.IO_ERROR, e);
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public final String toString() {
return "IBS[" + ibsPath + ", started=" + started + ", closed=" + closed + "]";
}
}