All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kolibrifx.plovercrest.server.Table Maven / Gradle / Ivy

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);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy