co.cask.tephra.persist.LocalFileTransactionStateStorage.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.tephra.persist.LocalFileTransactionStateStorage.java

Source

/*
 * Copyright  2012-2014 Cask Data, Inc.
 *
 * 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.
 */

package co.cask.tephra.persist;

import co.cask.tephra.TxConstants;
import co.cask.tephra.metrics.MetricsCollector;
import co.cask.tephra.snapshot.SnapshotCodecProvider;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.common.primitives.Longs;
import com.google.inject.Inject;
import org.apache.hadoop.conf.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Persists transaction snapshots and write-ahead logs to files on the local filesystem.
 */
public class LocalFileTransactionStateStorage extends AbstractTransactionStateStorage {
    private static final String TMP_SNAPSHOT_FILE_PREFIX = ".in-progress.";
    private static final String SNAPSHOT_FILE_PREFIX = "snapshot.";
    private static final String LOG_FILE_PREFIX = "txlog.";
    private static final Logger LOG = LoggerFactory.getLogger(LocalFileTransactionStateStorage.class);
    static final int BUFFER_SIZE = 16384;

    private static final FilenameFilter SNAPSHOT_FILE_FILTER = new FilenameFilter() {
        @Override
        public boolean accept(File file, String s) {
            return s.startsWith(SNAPSHOT_FILE_PREFIX);
        }
    };

    private final String configuredSnapshotDir;
    private final MetricsCollector metricsCollector;
    private File snapshotDir;

    @Inject
    public LocalFileTransactionStateStorage(Configuration conf, SnapshotCodecProvider codecProvider,
            MetricsCollector metricsCollector) {
        super(codecProvider);
        this.configuredSnapshotDir = conf.get(TxConstants.Manager.CFG_TX_SNAPSHOT_LOCAL_DIR);
        this.metricsCollector = metricsCollector;
    }

    @Override
    protected void startUp() throws Exception {
        Preconditions.checkState(configuredSnapshotDir != null, "Snapshot directory is not configured.  Please set "
                + TxConstants.Manager.CFG_TX_SNAPSHOT_LOCAL_DIR + " in configuration.");
        snapshotDir = new File(configuredSnapshotDir);
    }

    @Override
    protected void shutDown() throws Exception {
        // nothing to do
    }

    @Override
    public String getLocation() {
        return snapshotDir.getAbsolutePath();
    }

    @Override
    public void writeSnapshot(TransactionSnapshot snapshot) throws IOException {
        // save the snapshot to a temporary file
        File snapshotTmpFile = new File(snapshotDir, TMP_SNAPSHOT_FILE_PREFIX + snapshot.getTimestamp());
        LOG.debug("Writing snapshot to temporary file {}", snapshotTmpFile);
        OutputStream out = Files.newOutputStreamSupplier(snapshotTmpFile).getOutput();
        boolean threw = true;
        try {
            codecProvider.encode(out, snapshot);
            threw = false;
        } finally {
            Closeables.close(out, threw);
        }

        // move the temporary file into place with the correct filename
        File finalFile = new File(snapshotDir, SNAPSHOT_FILE_PREFIX + snapshot.getTimestamp());
        if (!snapshotTmpFile.renameTo(finalFile)) {
            throw new IOException("Failed renaming temporary snapshot file " + snapshotTmpFile.getName() + " to "
                    + finalFile.getName());
        }

        LOG.debug("Completed snapshot to file {}", finalFile);
    }

    @Override
    public TransactionSnapshot getLatestSnapshot() throws IOException {
        InputStream is = getLatestSnapshotInputStream();
        if (is == null) {
            return null;
        }
        try {
            return readSnapshotFile(is);
        } finally {
            is.close();
        }
    }

    @Override
    public TransactionVisibilityState getLatestTransactionVisibilityState() throws IOException {
        InputStream is = getLatestSnapshotInputStream();
        if (is == null) {
            return null;
        }
        try {
            return codecProvider.decodeTransactionVisibilityState(is);
        } finally {
            is.close();
        }
    }

    private InputStream getLatestSnapshotInputStream() throws IOException {
        File[] snapshotFiles = snapshotDir.listFiles(SNAPSHOT_FILE_FILTER);
        TimestampedFilename mostRecent = null;
        for (File file : snapshotFiles) {
            TimestampedFilename tsFile = new TimestampedFilename(file);
            if (mostRecent == null || tsFile.compareTo(mostRecent) > 0) {
                mostRecent = tsFile;
            }
        }

        if (mostRecent == null) {
            LOG.info("No snapshot files found in {}", snapshotDir.getAbsolutePath());
            return null;
        }

        return new FileInputStream(mostRecent.getFile());
    }

    private TransactionSnapshot readSnapshotFile(InputStream is) throws IOException {
        return codecProvider.decode(is);
    }

    @Override
    public long deleteOldSnapshots(int numberToKeep) throws IOException {
        File[] snapshotFiles = snapshotDir.listFiles(SNAPSHOT_FILE_FILTER);
        if (snapshotFiles.length == 0) {
            return -1;
        }
        TimestampedFilename[] snapshotFilenames = new TimestampedFilename[snapshotFiles.length];
        for (int i = 0; i < snapshotFiles.length; i++) {
            snapshotFilenames[i] = new TimestampedFilename(snapshotFiles[i]);
        }
        Arrays.sort(snapshotFilenames, Collections.reverseOrder());
        if (snapshotFilenames.length <= numberToKeep) {
            // nothing to delete, just return the oldest timestamp
            return snapshotFilenames[snapshotFilenames.length - 1].getTimestamp();
        }
        int toRemoveCount = snapshotFilenames.length - numberToKeep;
        TimestampedFilename[] toRemove = new TimestampedFilename[toRemoveCount];
        System.arraycopy(snapshotFilenames, numberToKeep, toRemove, 0, toRemoveCount);
        int removedCnt = 0;
        for (int i = 0; i < toRemove.length; i++) {
            File currentFile = toRemove[i].getFile();
            LOG.debug("Removing old snapshot file {}", currentFile.getAbsolutePath());
            if (!toRemove[i].getFile().delete()) {
                LOG.error("Failed deleting snapshot file {}", currentFile.getAbsolutePath());
            } else {
                removedCnt++;
            }
        }
        long oldestTimestamp = snapshotFilenames[numberToKeep - 1].getTimestamp();
        LOG.info("Removed {} out of {} expected snapshot files older than {}", removedCnt, toRemoveCount,
                oldestTimestamp);
        return oldestTimestamp;
    }

    @Override
    public List<String> listSnapshots() throws IOException {
        File[] snapshots = snapshotDir.listFiles(SNAPSHOT_FILE_FILTER);
        return Lists.transform(Arrays.asList(snapshots), new Function<File, String>() {
            @Nullable
            @Override
            public String apply(@Nullable File input) {
                return input.getName();
            }
        });
    }

    @Override
    public List<TransactionLog> getLogsSince(long timestamp) throws IOException {
        File[] logFiles = snapshotDir.listFiles(new LogFileFilter(timestamp, Long.MAX_VALUE));
        TimestampedFilename[] timestampedFiles = new TimestampedFilename[logFiles.length];
        for (int i = 0; i < logFiles.length; i++) {
            timestampedFiles[i] = new TimestampedFilename(logFiles[i]);
        }
        // logs need to be processed in ascending order
        Arrays.sort(timestampedFiles);
        return Lists.transform(Arrays.asList(timestampedFiles),
                new Function<TimestampedFilename, TransactionLog>() {
                    @Nullable
                    @Override
                    public TransactionLog apply(@Nullable TimestampedFilename input) {
                        return new LocalFileTransactionLog(input.getFile(), input.getTimestamp(), metricsCollector);
                    }
                });
    }

    @Override
    public TransactionLog createLog(long timestamp) throws IOException {
        File newLogFile = new File(snapshotDir, LOG_FILE_PREFIX + timestamp);
        LOG.info("Creating new transaction log at {}", newLogFile.getAbsolutePath());
        return new LocalFileTransactionLog(newLogFile, timestamp, metricsCollector);
    }

    @Override
    public void deleteLogsOlderThan(long timestamp) throws IOException {
        File[] logFiles = snapshotDir.listFiles(new LogFileFilter(0, timestamp));
        int removedCnt = 0;
        for (File file : logFiles) {
            LOG.debug("Removing old transaction log {}", file.getPath());
            if (file.delete()) {
                removedCnt++;
            } else {
                LOG.warn("Failed to remove log file {}", file.getAbsolutePath());
            }
        }
        LOG.debug("Removed {} transaction logs older than {}", removedCnt, timestamp);
    }

    @Override
    public void setupStorage() throws IOException {
        // create the directory if it doesn't exist
        if (!snapshotDir.exists()) {
            if (!snapshotDir.mkdirs()) {
                throw new IOException("Failed to create directory " + configuredSnapshotDir
                        + " for transaction snapshot storage");
            }
        } else {
            Preconditions.checkState(snapshotDir.isDirectory(),
                    "Configured snapshot directory " + configuredSnapshotDir + " is not a directory!");
            Preconditions.checkState(snapshotDir.canWrite(),
                    "Configured snapshot directory " + configuredSnapshotDir + " exists but is not writable!");
        }
    }

    @Override
    public List<String> listLogs() throws IOException {
        File[] logs = snapshotDir.listFiles(new LogFileFilter(0, Long.MAX_VALUE));
        return Lists.transform(Arrays.asList(logs), new Function<File, String>() {
            @Nullable
            @Override
            public String apply(@Nullable File input) {
                return input.getName();
            }
        });
    }

    private static class LogFileFilter implements FilenameFilter {
        private final long startTime;
        private final long endTime;

        public LogFileFilter(long startTime, long endTime) {
            this.startTime = startTime;
            this.endTime = endTime;
        }

        @Override
        public boolean accept(File file, String s) {
            if (s.startsWith(LOG_FILE_PREFIX)) {
                String[] parts = s.split("\\.");
                if (parts.length == 2) {
                    try {
                        long fileTime = Long.parseLong(parts[1]);
                        return fileTime >= startTime && fileTime < endTime;
                    } catch (NumberFormatException ignored) {
                        LOG.warn("Filename {} did not match the expected pattern prefix.<timestamp>", s);
                    }
                }
            }
            return false;
        }
    }

    /**
     * Represents a filename composed of a prefix and a ".timestamp" suffix.  This is useful for manipulating both
     * snapshot and transaction log filenames.
     */
    private static class TimestampedFilename implements Comparable<TimestampedFilename> {
        private File file;
        private String prefix;
        private long timestamp;

        public TimestampedFilename(File file) {
            this.file = file;
            String[] parts = file.getName().split("\\.");
            if (parts.length != 2) {
                throw new IllegalArgumentException(
                        "Filename " + file.getName() + " did not match the expected pattern prefix.timestamp");
            }
            prefix = parts[0];
            timestamp = Long.parseLong(parts[1]);
        }

        public File getFile() {
            return file;
        }

        public String getPrefix() {
            return prefix;
        }

        public long getTimestamp() {
            return timestamp;
        }

        @Override
        public int compareTo(TimestampedFilename other) {
            int res = prefix.compareTo(other.getPrefix());
            if (res == 0) {
                res = Longs.compare(timestamp, other.getTimestamp());
            }
            return res;
        }
    }

}