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

io.permazen.kv.raft.Log 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.raft.msg.AppendRequest;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.concurrent.GuardedBy;

/**
 * Raft log information.
 *
 * 

* Holds unapplied as well as recently applied log entries, as well as the last applied index, term, and configuration. */ final class Log { public static final int MIN_APPLIED = 10; // min # applied log entries to keep around public static final int MAX_APPLIED = 2000; // max # applied log entries to keep around private final RaftKVDatabase raft; @GuardedBy("raft") private final LogEntry[] applied = new LogEntry[MAX_APPLIED]; // already applied log entries (circular buffer) @GuardedBy("raft") private int numApplied; // the number of valid entries in "applied" @GuardedBy("raft") private final ArrayList unapplied = new ArrayList<>(); // unapplied log entries (empty if unconfigured) @GuardedBy("raft") private long lastAppliedTerm; // last applied term (zero if unconfigured) @GuardedBy("raft") private long lastAppliedIndex; // last applied index (zero if unconfigured) @GuardedBy("raft") private HashMap lastAppliedConfig = new HashMap<>(); // last applied config (empty if none) // Constructor Log(RaftKVDatabase raft) { Preconditions.checkArgument(raft != null); this.raft = raft; } // Reset public void reset(boolean deleteFiles) { this.reset(0, 0, Collections.emptyMap(), deleteFiles); } public void reset(long lastAppliedTerm, long lastAppliedIndex, Map config, boolean deleteFiles) { this.reset(lastAppliedTerm, lastAppliedIndex, Collections.emptyList(), config, deleteFiles); } public void reset(long lastAppliedTerm, long lastAppliedIndex, List entries, Map config, boolean deleteFiles) { // Sanity check Preconditions.checkArgument(lastAppliedTerm >= 0); Preconditions.checkArgument(lastAppliedIndex >= 0); Preconditions.checkArgument(entries != null); Preconditions.checkArgument(config != null); Preconditions.checkArgument(entries.isEmpty() || (entries.get(0).getIndex() <= lastAppliedIndex + 1 && entries.get(entries.size() - 1).getIndex() >= lastAppliedIndex)); // Optionally delete log entry files on disk if (deleteFiles) { // Delete applied log files for (int i = 0; i < this.applied.length; i++) { final LogEntry logEntry = this.applied[i]; if (logEntry != null) this.raft.deleteFile(logEntry.getFile(), "old log file"); } // Delete unapplied log files for (LogEntry logEntry : this.unapplied) this.raft.deleteFile(logEntry.getFile(), "old log file"); } // Reset state Arrays.fill(this.applied, null); this.numApplied = 0; this.unapplied.clear(); this.lastAppliedTerm = lastAppliedTerm; this.lastAppliedIndex = lastAppliedIndex; this.lastAppliedConfig.clear(); this.lastAppliedConfig.putAll(config); assert this.checkState(); // Import log entries long prevIndex = -1; long prevTerm = -1; for (LogEntry logEntry : entries) { final long index = logEntry.getIndex(); final long term = logEntry.getTerm(); Preconditions.checkArgument(prevIndex == -1 || index == prevIndex + 1); Preconditions.checkArgument(prevTerm == -1 || term >= prevTerm); if (index > this.lastAppliedIndex) this.unapplied.add(logEntry); else { final int appliedSlot = this.getAppliedSlot(index); if (appliedSlot != -1) { assert this.applied[appliedSlot] == null; this.applied[appliedSlot] = logEntry; this.numApplied++; } else this.raft.deleteFile(logEntry.getFile(), "old log file"); } prevIndex = index; prevTerm = term; } Preconditions.checkArgument(prevIndex == -1 || prevIndex >= lastAppliedIndex); assert this.checkState(); } // Accessors /** * Get the index of the first {@link LogEntry} for which we know the term. */ public long getFirstIndex() { assert Thread.holdsLock(this.raft); return this.numApplied > 0 ? this.lastAppliedIndex - this.numApplied + 1 : this.lastAppliedIndex; } /** * Get the index of the last {@link LogEntry} for which we know the term. * *

* This is equal to the index of the last unapplied log entry, or else {@link #getLastAppliedIndex} * if there are no unapplied log entries. */ public long getLastIndex() { assert Thread.holdsLock(this.raft); return this.lastAppliedIndex + this.unapplied.size(); } /** * Get the term corresponding to {@link #getLastIndex}. */ public long getLastTerm() { return this.getTermAtIndex(this.getLastIndex()); } /** * Get the log entries not yet applied to the state machine. * * @return an immutable list */ public List getUnapplied() { assert Thread.holdsLock(this.raft); return Collections.unmodifiableList(this.unapplied); } /** * Get the number of already applied log entries currently retained. */ public int getNumApplied() { assert Thread.holdsLock(this.raft); return this.numApplied; } /** * Get the number of log entries not yet applied to the state machine. */ public int getNumUnapplied() { assert Thread.holdsLock(this.raft); return this.unapplied.size(); } public int getNumTotal() { return this.getNumApplied() + getNumUnapplied(); } /** * Get the index of the log entry last applied to the state machine. */ public long getLastAppliedIndex() { assert Thread.holdsLock(this.raft); return this.lastAppliedIndex; } /** * Get the term associated with {@link #getLastAppliedIndex}. */ public long getLastAppliedTerm() { assert Thread.holdsLock(this.raft); return this.lastAppliedTerm; } /** * Get the config associated with {@link #getLastAppliedIndex}. */ public Map getLastAppliedConfig() { assert Thread.holdsLock(this.raft); return Collections.unmodifiableMap(this.lastAppliedConfig); } /** * Get the term of the unapplied log entry at the specified index. * * @param index log index; must correspond to an unapplied log entry or be equal to {@link #getLastAppliedIndex} * @throws IllegalArgumentException if {@code index} is bogus */ public long getTermAtIndex(long index) { assert Thread.holdsLock(this.raft); Preconditions.checkArgument(index >= this.lastAppliedIndex); Preconditions.checkArgument(index <= this.getLastIndex()); return index == this.lastAppliedIndex ? this.lastAppliedTerm : this.unapplied.get((int)(index - this.lastAppliedIndex - 1)).getTerm(); } /** * Get the unapplied log entry at the specified index. * * @param index log index; must correspond to an unapplied log entry * @throws IllegalArgumentException if {@code index} is bogus */ public LogEntry getEntryAtIndex(long index) { assert Thread.holdsLock(this.raft); Preconditions.checkArgument(index > this.lastAppliedIndex); Preconditions.checkArgument(index <= this.getLastIndex()); return this.unapplied.get((int)(index - this.lastAppliedIndex - 1)); } /** * Get the term of the log entry at the specified index, if known. * * @param index log index; may be anything * @return log entry term or zero if unknown */ public long getTermAtIndexIfKnown(long index) { assert Thread.holdsLock(this.raft); if (index > this.lastAppliedIndex) { if (index > this.getLastIndex()) return 0; return this.unapplied.get((int)(index - this.lastAppliedIndex - 1)).getTerm(); } if (index < this.lastAppliedIndex) { final int appliedSlot = this.getAppliedSlot(index); if (appliedSlot == -1) return 0; final LogEntry logEntry = this.applied[appliedSlot]; return logEntry != null ? logEntry.getTerm() : 0; } return this.lastAppliedTerm; } /** * Get the applied or unapplied log entry at the specified index, if known. * * @param index log index; may be anything * @return log entry or null if unknown */ public LogEntry getEntryAtIndexIfKnown(long index) { assert Thread.holdsLock(this.raft); if (index > this.lastAppliedIndex) { index -= this.lastAppliedIndex + 1; return index < this.unapplied.size() ? this.unapplied.get((int)index) : null; } final int appliedSlot = this.getAppliedSlot(index); if (appliedSlot == -1) return null; return this.applied[appliedSlot]; } /** * Reconstruct the current config by starting with the last applied config and applying * configuration deltas from unapplied log entries. */ public Map buildCurrentConfig() { // Start with last applied config @SuppressWarnings("unchecked") final HashMap config = (HashMap)this.lastAppliedConfig.clone(); // Apply any changes found in uncommitted log entries for (LogEntry logEntry : this.unapplied) logEntry.applyConfigChange(config); // Done return config; } public long getUnappliedLogMemoryUsage() { assert Thread.holdsLock(this.raft); long total = 0; for (LogEntry logEntry : this.unapplied) total += logEntry.getFileSize(); return total; } // Mutators /** * Move the next unapplied {@link LogEntry} to the applied log and update "last applied" state. */ public void applyNextLogEntry() { // Sanity check assert Thread.holdsLock(this.raft); // Remove entry from "unapplied" list final LogEntry logEntry = this.unapplied.remove(0); // Update "last applied" info final long index = ++this.lastAppliedIndex; assert logEntry.getIndex() == index; this.lastAppliedTerm = logEntry.getTerm(); logEntry.applyConfigChange(this.lastAppliedConfig); // Add entry to "applied" list final int appliedSlot = this.getAppliedSlot(index); assert appliedSlot != -1; final LogEntry oldEntry = this.applied[appliedSlot]; assert (oldEntry != null) == (this.numApplied == MAX_APPLIED); if (oldEntry != null) this.raft.deleteFile(oldEntry.getFile(), "old log file"); this.applied[appliedSlot] = logEntry; if (this.numApplied < MAX_APPLIED) this.numApplied++; // Discard associated Writes object to save memory logEntry.discardWrites(); assert this.checkState(); } /** * Add a new {@link LogEntry} to the unapplied log. */ public void addLogEntry(LogEntry logEntry) { assert Thread.holdsLock(this.raft); Preconditions.checkArgument(logEntry.getIndex() == this.getLastIndex() + 1); this.unapplied.add(logEntry); assert this.checkState(); } /** * Discard unapplied log entries at or above the specified index because they have been overwritten. * * @param startingIndex starting index for discard */ public void discardLogEntries(final long startingIndex, AppendRequest msg) { // Sanity check assert Thread.holdsLock(this.raft); Preconditions.checkArgument(startingIndex > this.lastAppliedIndex); final int minListIndex = (int)(startingIndex - this.lastAppliedIndex - 1); final int maxListIndex = this.unapplied.size(); if (minListIndex >= maxListIndex) return; // Delete log entries and associated files final List conflictList = this.unapplied.subList(minListIndex, maxListIndex); for (LogEntry logEntry : conflictList) { if (this.raft.logger.isDebugEnabled()) this.raft.debug("deleting log entry " + logEntry + " overwritten by " + msg); this.raft.deleteFile(logEntry.getFile(), "overwritten log file"); } conflictList.clear(); assert this.checkState(); } /** * Discard applied log entries up to the specified index because they are no longer needed. * * @param maxDiscardIndex maximum index of applied log entries to discard * @throws IllegalArgumentException if {@code maxDiscardIndex} is greater than the last applied index */ public void discardAppliedLogEntries(long maxDiscardIndex) { // Sanity check assert Thread.holdsLock(this.raft); Preconditions.checkArgument(maxDiscardIndex <= this.lastAppliedIndex); final long minDiscardIndex = this.lastAppliedIndex - this.numApplied + 1; // Keep a minimum number of applied log entries just for good measure maxDiscardIndex = Math.min(maxDiscardIndex, this.lastAppliedIndex - MIN_APPLIED); // Delete applied log entries and associated files for (long index = minDiscardIndex; index <= maxDiscardIndex; index++) { final int appliedSlot = this.getAppliedSlot(index); assert appliedSlot != -1; final LogEntry logEntry = this.applied[appliedSlot]; assert logEntry != null; if (this.raft.logger.isDebugEnabled()) this.raft.debug("deleting log entry " + logEntry + " no longer needed"); this.raft.deleteFile(logEntry.getFile(), "no longer needed"); this.applied[appliedSlot] = null; this.numApplied--; } assert this.checkState(); } // Other /** * Get the array index in {@code this.applied} for the already-applied log entry with the given index. * *

* If this returns a value >= 0, there may or may not actually be a {@link LogEntry} there. * * @param index log index * @return array index in {@code this.applied} corresponding to {@code index}, or -1 if {@code index} is too old * @throws IllegalArgumentException if {@code index} is greater than the last applied index */ private int getAppliedSlot(long index) { final long offset = this.lastAppliedIndex - index; Preconditions.checkArgument(offset >= 0); if (index <= 0 || offset >= MAX_APPLIED) return -1; return (int)(index % MAX_APPLIED); } boolean checkState() { // Check unapplied entries assert this.lastAppliedTerm >= 0; assert this.lastAppliedIndex >= 0; if (!this.unapplied.isEmpty()) { long index = this.lastAppliedIndex; long term = this.lastAppliedTerm; for (LogEntry logEntry : this.unapplied) { assert logEntry.getIndex() == index + 1; assert logEntry.getTerm() >= term; index = logEntry.getIndex(); term = logEntry.getTerm(); } } // Check configured vs. unconfigured if (this.getLastIndex() > 0) { assert !this.buildCurrentConfig().isEmpty(); assert this.getLastTerm() > 0; assert this.getLastIndex() > 0; assert this.getNumApplied() >= 0; assert this.getNumApplied() <= MAX_APPLIED; for (int i = 0; i <= MAX_APPLIED; i++) { final long index = this.lastAppliedIndex - i; final int appliedSlot = this.getAppliedSlot(index); if (index <= 0 || i == MAX_APPLIED) { assert appliedSlot == -1; continue; } assert appliedSlot != -1; final LogEntry logEntry = this.applied[appliedSlot]; assert (logEntry != null) == (i < this.numApplied); } } else { assert this.lastAppliedTerm == 0; assert this.lastAppliedIndex == 0; assert this.lastAppliedConfig.isEmpty(); assert this.getNumApplied() == 0; assert this.getNumUnapplied() == 0; assert Arrays.equals(this.applied, new LogEntry[MAX_APPLIED]); } return true; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy