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

io.permazen.kv.raft.LogEntry Maven / Gradle / Ivy


/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package io.permazen.kv.raft;

import com.google.common.base.Preconditions;

import io.permazen.kv.mvcc.Mutations;
import io.permazen.kv.mvcc.Writes;
import io.permazen.util.Streams;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Comparator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dellroad.stuff.io.ByteBufferInputStream;

/**
 * Represents one unapplied Raft log entry.
 */
public class LogEntry {

    /**
     * Sorts instances by {@linkplain LogEntry#getIndex log index}.
     */
    public static final Comparator SORT_BY_INDEX = Comparator.comparingLong(LogEntry::getIndex);

    static final String LOG_FILE_PREFIX = "log-";
    static final String LOG_FILE_SUFFIX = ".bin";
    static final Pattern LOG_FILE_PATTERN = Pattern.compile(
      Pattern.quote(LOG_FILE_PREFIX) + "([0-9]{19})-([0-9]{19})" + Pattern.quote(LOG_FILE_SUFFIX));

    private final Timestamp createTime = new Timestamp();
    private final long term;
    private final long index;
    private final String[] configChange;
    private final long fileSize;
    private final File file;

    private Writes writes;
    private ByteBuffer content;

// Constructors

    /**
     * Constructor.
     *
     * @param term log entry term
     * @param index log entry index
     * @param logDir directory containing log files
     * @param data log entry data
     * @param fileSize the size of the file
     */
    LogEntry(final long term, final long index, final File logDir, final Data data, final long fileSize) {
        Preconditions.checkArgument(term > 0, "bogus term");
        Preconditions.checkArgument(index > 0, "bogus index");
        Preconditions.checkArgument(logDir != null, "null logDir");
        Preconditions.checkArgument(data != null, "null data");
        Preconditions.checkArgument(fileSize > 0, "invalid fileSize");
        this.term = term;
        this.index = index;
        this.writes = data.getWrites();
        this.configChange = data.getConfigChange();
        this.fileSize = fileSize;
        this.file = new File(logDir,
          String.format("%s%019d-%019d%s", LOG_FILE_PREFIX, this.getIndex(), this.getTerm(), LOG_FILE_SUFFIX));
    }

// Properties

    /**
     * Get the age of this log entry since instantiation.
     *
     * @return age in milliseconds
     */
    public int getAge() {
        return -this.createTime.offsetFromNow();
    }

    /**
     * Get the term of this instance.
     *
     * @return associated log entry term
     */
    public long getTerm() {
        return this.term;
    }

    /**
     * Get the index of this instance.
     *
     * @return associated log entry index
     */
    public long getIndex() {
        return this.index;
    }

    /**
     * Get the {@link Writes} associated with this entry.
     *
     * @return transaction writes
     * @throws IllegalStateException if this log entry has already been applied to the state machine
     */
    Writes getWrites() {
        Preconditions.checkState(this.writes != null, "entry already applied to state machine");
        return this.writes;
    }

    /**
     * Get the {@link Mutations} associated with this entry.
     *
     * 

* Unlike {@link #getWrites}, which is only guaranteed to be available prior to the application of this * log entry to the state machine, the {@link Mutations} (usable for conflict checking) are always available. * * @return transaction mutations */ Mutations getMutations() throws IOException { if (this.writes != null) return this.writes; return Writes.deserializeOnline(new ByteBufferInputStream(this.getContent())); } /** * Discard this instance's {@link Writes}. * *

* This is done after it has been applied to the state machine to save memory. * @see #getMutations * @see #getWrites */ void discardWrites() { this.writes = null; } /** * Get the cluster config change associated with this log entry, if any. * *

* The returned array is a copy; changes have no effect on this instance. * * @return cluster config change, or null if none * @see RaftKVTransaction#configChange */ public String[] getConfigChange() { return this.configChange != null ? this.configChange.clone() : null; } /** * Apply the cluster config change associated with this log entry, if any. * * @param config configuration to modify * @return true if {@code config} was modified, otherwise false */ boolean applyConfigChange(Map config) { if (this.configChange == null) return false; return this.configChange[1] != null ? !this.configChange[1].equals(config.put(this.configChange[0], this.configChange[1])) : config.remove(this.configChange[0]) != null; } /** * Get the size of the on-disk file for this log entry. * * @return log entry file size */ public long getFileSize() { return this.fileSize; } /** * Get the on-disk file for this log entry. * * @return log entry file */ public File getFile() { return this.file; } // File I/O /** * Get the serialized contents of this log entry by reading the file. */ ByteBuffer getContent() throws IOException { if (this.content == null) this.content = Util.readFile(this.getFile(), this.fileSize); return this.content.asReadOnlyBuffer(); } /** * Parse a log file name, extracting the log term and index. * * @param fileName file name * @return array of length two containing {@code [index, term]}, or null if parse fails * @throws IllegalArgumentException if {@code fileName} is null */ public static long[] parseFileName(String fileName) { Preconditions.checkArgument(fileName != null, "null fileName"); final Matcher matcher = LOG_FILE_PATTERN.matcher(fileName); if (!matcher.matches()) return null; final long index = Long.parseLong(matcher.group(1), 10); final long term = Long.parseLong(matcher.group(2), 10); return new long[] { index, term }; } /** * Create a {@link LogEntry} from the specified file. * * @param file log file * @param loadWrites true to load {@link Writes} into memory, false to skip * @return {@link LogEntry} instance, or null if {@code file} or {@code file}'s name is invalid * @throws NullPointerException if {@code file} is null * @throws IOException if an I/O error occurs * @throws IOException if file contains invalid data */ static LogEntry fromFile(File file, boolean loadWrites) throws IOException { // Parse file name final long[] parse = LogEntry.parseFileName(file.getName()); if (parse == null) throw new IOException(String.format("invalid log file name \"%s\"", file.getName())); final long index = parse[0]; final long term = parse[1]; // Get file length final long fileLength = Util.getLength(file); // Decode log entry final Data data; try (BufferedInputStream input = new BufferedInputStream(new FileInputStream(file), 4096)) { data = LogEntry.readData(input, loadWrites); } // Done return new LogEntry(term, index, file.getParentFile(), data, fileLength); } /** * Read log entry data from the specified input. * * @param input input stream * @param loadWrites true to load {@link Writes} into memory, false to skip * @return log entry data * @throws IOException if an I/O error occurs * @throws IOException if file contains invalid data */ static Data readData(InputStream input, boolean loadWrites) throws IOException { Preconditions.checkArgument(input != null, "null input"); // Get (or skip over) writes final Writes writes; try { if (loadWrites) writes = Writes.deserialize(input, true); // load writes else { final Mutations mutations = Writes.deserializeOnline(input); // skip over writes Streams.exhaust(mutations.getRemoveRanges()); Streams.exhaust(mutations.getPutPairs()); Streams.exhaust(mutations.getAdjustPairs()); writes = null; } } catch (IllegalArgumentException e) { throw new IOException("log entry input contains invalid content", e); } catch (RuntimeException e) { if (e.getCause() instanceof IOException) throw (IOException)e.getCause(); throw e; } // Get config change, if any final String[] configChange; final DataInputStream data = new DataInputStream(input); if (data.readBoolean()) { configChange = new String[2]; configChange[0] = data.readUTF(); if (data.readBoolean()) configChange[1] = data.readUTF(); } else configChange = null; // Verify end of file if (input.read() != -1) throw new IOException("log entry input contains trailing garbage"); // Done return new Data(writes, configChange); } /** * Write log entry data to the specified output. * * @param output output stream * @param data log entry data * @throws IOException if an I/O error occurs */ static void writeData(OutputStream output, Data data) throws IOException { Preconditions.checkArgument(output != null, "null output"); Preconditions.checkArgument(data != null, "null data"); data.getWrites().serialize(output); final DataOutputStream dataOutput = new DataOutputStream(output); final String[] configChange = data.getConfigChange(); dataOutput.writeBoolean(configChange != null); if (configChange != null) { dataOutput.writeUTF(configChange[0]); dataOutput.writeBoolean(configChange[1] != null); if (configChange[1] != null) dataOutput.writeUTF(configChange[1]); } dataOutput.flush(); } // Object @Override public String toString() { return this.getIndex() + "t" + this.getTerm() + (this.configChange != null ? (this.configChange[1] != null ? "+" + this.configChange[0] + "@" + this.configChange[1] : "-" + this.configChange[0]) : ""); } // Data /** * Data associated with a {@link LogEntry}. */ static class Data { private final Writes writes; private final String[] configChange; /** * Constructor. * * @param writes key/value mutations, or null for none * @param configChange cluster config change (identity, address), or null for none */ Data(Writes writes, String[] configChange) { Preconditions.checkArgument(configChange == null || (configChange.length == 2 && configChange[0] != null)); this.writes = writes != null ? writes.readOnlySnapshot() : null; this.configChange = configChange; } public Writes getWrites() { return this.writes; } public String[] getConfigChange() { return this.configChange; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy