com.kolibrifx.plovercrest.server.Table Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of plovercrest-server Show documentation
Show all versions of plovercrest-server Show documentation
Plovercrest server library.
The newest version!
/*
* Copyright (c) 2010-2017, KolibriFX AS. Licensed under the Apache License, version 2.0.
*/
package com.kolibrifx.plovercrest.server;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.log4j.Logger;
import com.kolibrifx.plovercrest.client.PlovercrestException;
import com.kolibrifx.plovercrest.client.TableLockedException;
import com.kolibrifx.plovercrest.server.internal.DeferredTableInfo;
import com.kolibrifx.plovercrest.server.internal.EventDispatcher;
import com.kolibrifx.plovercrest.server.internal.EventType;
import com.kolibrifx.plovercrest.server.internal.PathUtils;
import com.kolibrifx.plovercrest.server.internal.TableInfoSerializer;
import com.kolibrifx.plovercrest.server.internal.TableListenerCollection;
import com.kolibrifx.plovercrest.server.internal.engine.DeferredReader;
import com.kolibrifx.plovercrest.server.internal.engine.DeferredTableMapper;
import com.kolibrifx.plovercrest.server.internal.engine.LeastRecentlyUsedCache;
/**
* The Table
class represents a memory mapped table on disk. The client-side
* Table
class forwards its operations to this implementation.
*/
public final class Table {
private static final int MAX_FILE_PREFIX_LENGTH = 248; // 255 - ".fanout"
private static final long UNMAP_WORKAROUND_TIMEOUT_MS = 1000;
private static final Logger log = Logger.getLogger(Table.class);
private final DeferredTableInfo deferredTableInfo;
private final File infoFile;
private final File dataFile;
private final File fanOutFile;
private final TableWriter writer;
private final DeferredTableMapper mapper;
private final EventDispatcher dispatcher;
private volatile boolean closed = false;
private TableListenerCollection tableListeners;
private Table(final File infoFile,
final File dataFile,
final File fanOutFile,
final EventDispatcher dispatcher,
final LeastRecentlyUsedCache cache) {
this.deferredTableInfo = new DeferredTableInfo(infoFile);
this.infoFile = infoFile;
this.dataFile = dataFile;
this.fanOutFile = fanOutFile;
this.dispatcher = dispatcher;
this.mapper = new DeferredTableMapper(this, cache);
this.writer = new TableWriter(this, mapper);
}
public String getName() {
return deferredTableInfo.getName();
}
private static boolean tryDelete(final File file) {
if (!file.exists()) {
return true;
}
final boolean result = file.delete();
if (!result) {
log.error("Failed to delete file " + file.getAbsolutePath());
// Thread.dumpStack();
}
return result;
}
// Workaround for bugs:
// http://bugs.sun.com/view_bug.do?bug_id=4724038
// http://bugs.sun.com/view_bug.do?bug_id=4715154
private static boolean tryDeleteDataFile(final File dataFile, final long timeoutMs) {
if (!dataFile.exists() || dataFile.delete()) {
return true;
}
final long startTime = System.currentTimeMillis();
while (dataFile.exists() && !dataFile.delete()) {
// Yes, this is horrible.
System.gc();
System.runFinalization();
Thread.yield();
if ((System.currentTimeMillis() - startTime) > timeoutMs) {
// one final attempt before giving up:
if (dataFile.delete()) {
return true;
}
log.error("Timed out attempting to delete data file " + dataFile);
return false;
}
}
return true;
}
private static boolean tryRenameDataFile(final File dataFile, final File dest, final long timeoutMs) {
if (dataFile.renameTo(dest)) {
return true;
}
final long startTime = System.currentTimeMillis();
while (!dataFile.renameTo(dest)) {
// Yes, this is still horrible.
System.gc();
System.runFinalization();
Thread.yield();
if ((System.currentTimeMillis() - startTime) > timeoutMs) {
// one final attempt before giving up:
if (dataFile.renameTo(dest)) {
return true;
}
log.error("Timed out attempting to rename data file " + dataFile + " to " + dest);
return false;
}
}
return true;
}
boolean delete() {
unsafeClose();
boolean result = tryDelete(infoFile);
result = tryDeleteDataFile(dataFile, UNMAP_WORKAROUND_TIMEOUT_MS) && result;
result = tryDelete(fanOutFile) && result;
return result;
}
/**
* Renames the table. Will also close it, so this object should no longer be used after a
* rename.
*
* Note: this is currently not an atomic operation, and there is no rollback if interrupted.
*/
boolean rename(final String newName) {
close();
final String dataPath = infoFile.getParent();
final TableInfo info = deferredTableInfo.getInfo();
final TableInfo newInfo = new TableInfo(newName, info.getFanOutDistance(), info.getMetadata());
final File newInfoFile = constructFileName(dataPath, newName, ".info");
try {
boolean ok = newInfoFile.createNewFile();
TableInfoSerializer.write(newInfo, newInfoFile);
ok = tryRenameDataFile(dataFile, constructFileName(dataPath, newName, ".data"),
UNMAP_WORKAROUND_TIMEOUT_MS);
ok = fanOutFile.renameTo(constructFileName(dataPath, newName, ".fanout")) && ok;
ok = infoFile.delete() && ok;
return ok;
} catch (final IOException e) {
throw new PlovercrestException("IO error during rename (" + getName() + " -> " + newName + ")", e);
}
}
static File constructFileName(final String dir, final String tableName, final String suffix) {
return new File(dir + "/" + PathUtils.sanitizeName(tableName) + suffix);
}
private static void tryDeleteDataFiles(final String dataPath, final File infoFile) {
final File dataFile = new File(dataPath + "/" + infoFile.getName().replace(".info", ".data"));
final File fanOutFile = new File(dataPath + "/" + infoFile.getName().replace(".info", ".fanout"));
for (final File file : new File[] { infoFile, dataFile, fanOutFile }) {
if (file.exists() && !file.delete()) {
log.warn("Failed to delete file: " + file);
}
}
}
static Table create(final String dataPath, final TableInfo info, final EventDispatcher dispatcher,
final LeastRecentlyUsedCache cache) {
final String name = info.getName();
String fileName = PathUtils.sanitizeName(name);
// Assume that the file name encoding uses no more bytes than UTF-8
if (fileName.getBytes(StandardCharsets.UTF_8).length > MAX_FILE_PREFIX_LENGTH) {
fileName = TableInfoSerializer.generateUniqueFileNamePrefix();
}
final File infoFile = new File(dataPath + "/" + fileName + ".info");
final File dataFile = new File(dataPath + "/" + fileName + ".data");
final File fanOutFile = new File(dataPath + "/" + fileName + ".fanout");
if (infoFile.exists()) {
throw new PlovercrestException("Table already exists : " + name);
}
// If there is no .info file, but a .data or .fanout file exists, something has probably gone wrong during deletion.
// It is better to delete the files now than to be stuck in a state where no new table can be created.
if (dataFile.exists() || fanOutFile.exists()) {
log.warn("Deleting leftover files for table: " + name);
tryDeleteDataFiles(dataPath, infoFile);
}
if (dataFile.exists()) {
throw new PlovercrestException("Failed to delete leftover data file for table: " + name);
}
if (fanOutFile.exists()) {
throw new PlovercrestException("Failed to delete leftover fanout file for table: " + name);
}
try {
// touch the files
if (!infoFile.createNewFile() || !dataFile.createNewFile() || !fanOutFile.createNewFile()) {
throw new PlovercrestException("Failed to create table " + info.getName());
}
// serialize to info
// writeInfo(infoFile, info);
TableInfoSerializer.write(info, infoFile);
return new Table(infoFile, dataFile, fanOutFile, dispatcher, cache);
} catch (final IOException e) {
throw new PlovercrestException("Failed to initialize table " + info.getName(), e);
}
}
static Table open(final String dataPath, final File infoFile, final EventDispatcher dispatcher,
final LeastRecentlyUsedCache cache) {
Table table = null;
boolean tableLocked = false;
try {
final File dataFile = new File(dataPath + "/" + infoFile.getName().replace(".info", ".data"));
final File fanOutFile = new File(dataPath + "/" + infoFile.getName().replace(".info", ".fanout"));
table = new Table(infoFile, dataFile, fanOutFile, dispatcher, cache);
} catch (final TableLockedException e) {
tableLocked = true;
throw e;
} catch (final PlovercrestException e) {
throw e;
} catch (final Exception e) {
log.error(e.getMessage(), e);
throw new PlovercrestException("Unknown exception " + e.getClass().getName() + " while reading table info",
e);
} finally {
if (table == null && !tableLocked) {
log.error("Failed to open table, corrupt info file? " + infoFile);
}
}
return table;
}
public TableInfo getInfo() {
return deferredTableInfo.getInfo();
}
public TableWriter getWriter() {
return writer;
}
public File dataFile() {
return dataFile;
}
public File fanOutFile() {
return fanOutFile;
}
/**
* Creates a fresh reader.
*/
public TableReader getReader() {
return new TableReader(new DeferredReader(mapper));
}
/**
* Creates a reader at the given entry index.
*/
public TableReader getReaderAtEntryIndex(final long index) {
final TableReader reader = new TableReader(new DeferredReader(mapper));
reader.seekToEntryIndex(index);
return reader;
}
/**
* Free resources used by this table. The effect of using or creating readers or writers after
* calling this function is undefined.
*
* @param flush
* If true
, flush (fsync) data before close, and attempt to unmap the
* file (even though Java doesn't want us to). This is a potentially slow operation,
* but recommended during a clean shutdown to minimize the risk of data loss. Not
* needed before deleting a table.
*/
private void doClose(final boolean flush) {
if (closed) {
return;
}
closed = true; // race condition, but not important
mapper.close(flush);
// Since MappedByteBuffer does not provide a way to unmap/close the
// mapped region, calling
// finalizers is the only way to free resources.
System.runFinalization();
}
/**
* Free resources used by this table. The effect of using or creating readers or writers after
* calling this function is undefined. Data is flushed (fsync'ed) before closing, and an attempt
* to unmap the file is done (even though Java doesn't want us to).
*/
public void close() {
doClose(true);
}
/**
* Close the table without flushing data. Should only be called before deleting a table.
*/
private void unsafeClose() {
doClose(false);
}
/**
* Returns the earliest timestamp of this table, or -1 if the table is empty.
*/
public long getFirstTimestamp() {
return mapper.getFirstTimestamp();
}
/**
* Returns the latest timestamp of this table, or -1 if the table is empty.
*/
public long getLastTimestamp() {
return mapper.getLastTimestamp();
}
/**
* Returns the last timestamp for which the last element is known to be valid (≥
* getLastTimestamp).
*/
public long getLastValidTimestamp() {
return mapper.getLastValidTimestamp();
}
/**
* Current data length, in bytes.
*/
public long getDataLength() {
return mapper.getDataLength();
}
/**
* The number of elements in the table.
*/
public long getEntryCount() {
return mapper.getEntryCount();
}
public TableListenerCollection getTableListeners() {
return tableListeners;
}
public void setTableListeners(final TableListenerCollection tableListeners) {
this.tableListeners = tableListeners;
}
public void dispatchEvent(final EventType type) {
if (tableListeners == null) {
return;
}
tableListeners.dispatch(dispatcher, getName(), type);
}
}