io.permazen.kv.raft.Role Maven / Gradle / Ivy
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.kv.raft;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Bytes;
import io.permazen.kv.KVTransactionException;
import io.permazen.kv.KeyRange;
import io.permazen.kv.RetryTransactionException;
import io.permazen.kv.mvcc.Mutations;
import io.permazen.kv.mvcc.Reads;
import io.permazen.kv.mvcc.Writes;
import io.permazen.kv.raft.msg.AppendRequest;
import io.permazen.kv.raft.msg.AppendResponse;
import io.permazen.kv.raft.msg.CommitRequest;
import io.permazen.kv.raft.msg.CommitResponse;
import io.permazen.kv.raft.msg.GrantVote;
import io.permazen.kv.raft.msg.InstallSnapshot;
import io.permazen.kv.raft.msg.Message;
import io.permazen.kv.raft.msg.PingRequest;
import io.permazen.kv.raft.msg.PingResponse;
import io.permazen.kv.raft.msg.RequestVote;
import io.permazen.util.LongEncoder;
import java.io.IOException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
/**
* Common superclass for the three roles played by a Raft node:
* {@linkplain LeaderRole leader}, {@linkplain FollowerRole follower}, and {@linkplain CandidateRole candidate}.
*/
public abstract class Role {
final Logger log;
final RaftKVDatabase raft;
final Service checkReadyTransactionsService = new Service(this, "check ready transactions") {
@Override
public void run() {
Role.this.checkReadyTransactions();
}
};
final Service checkWaitingTransactionsService = new Service(this, "check waiting transactions") {
@Override
public void run() {
Role.this.checkWaitingTransactions();
}
};
// NOTE: use of this service requires that 'checkWaitingTransactionsService' be scheduled first!
final Service applyCommittedLogEntriesService = new Service(this, "apply committed logs") {
@Override
public void run() {
Role.this.applyCommittedLogEntries();
}
};
final Service triggerKeyWatchesService = new Service(this, "trigger key watches") {
@Override
public void run() {
Role.this.triggerKeyWatches();
}
};
// Constructors
Role(RaftKVDatabase raft) {
this.raft = raft;
this.log = this.raft.log;
assert Thread.holdsLock(this.raft);
}
// Status
/**
* Get the {@link RaftKVDatabase} with which this instance is associated.
*
* @return associated database
*/
public RaftKVDatabase getKVDatabase() {
return this.raft;
}
// Lifecycle
void setup() {
assert Thread.holdsLock(this.raft);
this.raft.requestService(this.checkReadyTransactionsService);
this.raft.requestService(this.checkWaitingTransactionsService);
this.raft.requestService(this.applyCommittedLogEntriesService);
}
void shutdown() {
// Sanity check
assert Thread.holdsLock(this.raft);
// Fail any (read-only) transactions with a minimum lease timeout, because they won't be valid for a new leader
for (RaftKVTransaction tx : new ArrayList<>(this.raft.openTransactions.values())) {
if (!tx.getState().equals(TxState.COMPLETED) && tx.getCommitLeaderLeaseTimeout() != null) {
assert tx.hasCommitInfo();
this.raft.fail(tx, new RetryTransactionException(tx, "leader was deposed during leader lease timeout wait"));
}
}
// Cleanup role-specific state
for (RaftKVTransaction tx : this.raft.openTransactions.values())
this.cleanupForTransaction(tx);
}
// Service
abstract void outputQueueEmpty(String address);
/**
* Check transactions in the {@link TxState#COMMIT_READY} state to see if we can advance them.
*/
void checkReadyTransactions() {
assert Thread.holdsLock(this.raft);
for (RaftKVTransaction tx : new ArrayList<>(this.raft.openTransactions.values())) {
if (TxState.COMMIT_READY.equals(tx.getState()))
new CheckReadyTransactionService(this, tx).run();
}
}
/**
* Check transactions in the {@link TxState#COMMIT_WAITING} state to see if they are committed yet.
* We invoke this service method whenever our {@code commitIndex} advances.
*/
void checkWaitingTransactions() {
assert Thread.holdsLock(this.raft);
for (RaftKVTransaction tx : new ArrayList<>(this.raft.openTransactions.values())) {
if (TxState.COMMIT_WAITING.equals(tx.getState()))
new CheckWaitingTransactionService(this, tx).run();
}
}
/**
* Apply committed but unapplied log entries to the state machine.
* We invoke this service method whenever log entries are added or our {@code commitIndex} advances.
*
*
* Note: checkWaitingTransactions() must have been invoked already when this method is invoked.
*/
void applyCommittedLogEntries() {
// Sanity check
assert Thread.holdsLock(this.raft);
assert this.checkRebasableAndCommittableUpToDate();
// Determine how many committed log entries we can apply to the state machine at this time
int numEntriesToApply = 0;
while (this.raft.lastAppliedIndex + numEntriesToApply < this.raft.commitIndex
&& this.mayApplyLogEntry(this.raft.raftLog.get(numEntriesToApply)))
numEntriesToApply++;
final long maxAppliedIndex = this.raft.lastAppliedIndex + numEntriesToApply;
assert maxAppliedIndex <= this.raft.commitIndex;
// Apply committed log entries to the state machine
while (this.raft.lastAppliedIndex < maxAppliedIndex) {
// Grab the first unwritten log entry
final LogEntry logEntry = this.raft.raftLog.get(0);
assert logEntry.getIndex() == this.raft.lastAppliedIndex + 1;
// Get the current config as of the log entry we're about to apply
final HashMap logEntryConfig = new HashMap<>(this.raft.lastAppliedConfig);
logEntry.applyConfigChange(logEntryConfig);
// Prepare combined Mutations containing prefixed log entry changes plus my own
final Writes logWrites = logEntry.getWrites();
final Writes myWrites = new Writes();
myWrites.getPuts().put(RaftKVDatabase.LAST_APPLIED_TERM_KEY, LongEncoder.encode(logEntry.getTerm()));
myWrites.getPuts().put(RaftKVDatabase.LAST_APPLIED_INDEX_KEY, LongEncoder.encode(logEntry.getIndex()));
myWrites.getPuts().put(RaftKVDatabase.LAST_APPLIED_CONFIG_KEY, this.raft.encodeConfig(logEntryConfig));
final byte[] stateMachinePrefix = this.raft.getStateMachinePrefix();
final Mutations mutations = new Mutations() {
@Override
public Iterable getRemoveRanges() {
return Iterables.transform(logWrites.getRemoveRanges(), range -> range.prefixedBy(stateMachinePrefix));
}
@Override
public Iterable> getPutPairs() {
return Iterables.concat(
Iterables.transform(logWrites.getPutPairs(),
entry -> new AbstractMap.SimpleEntry<>(Bytes.concat(stateMachinePrefix, entry.getKey()), entry.getValue())),
myWrites.getPutPairs());
}
@Override
public Iterable> getAdjustPairs() {
return Iterables.transform(logWrites.getAdjustPairs(),
entry -> new AbstractMap.SimpleEntry<>(Bytes.concat(stateMachinePrefix, entry.getKey()), entry.getValue()));
}
};
// Apply updates to the key/value store; when applying the last one, durably persist
if (this.log.isDebugEnabled())
this.debug("applying committed log entry " + logEntry + " to key/value store");
try {
this.raft.kv.mutate(mutations, !this.raft.disableSync && this.raft.lastAppliedIndex == maxAppliedIndex);
} catch (Exception e) {
if (e instanceof RuntimeException && e.getCause() instanceof IOException)
e = (IOException)e.getCause();
this.error("error applying log entry " + logEntry + " to key/value store", e);
break;
}
// Update in-memory state
assert logEntry.getIndex() == this.raft.lastAppliedIndex + 1;
this.raft.incrementLastAppliedIndex(logEntry.getTerm());
logEntry.applyConfigChange(this.raft.lastAppliedConfig);
assert this.raft.currentConfig.equals(this.raft.buildCurrentConfig());
// Delete the log entry
this.raft.raftLog.remove(0);
this.raft.deleteFile(logEntry.getFile(), "applied log file");
}
}
// Assertion check
boolean checkRebasableAndCommittableUpToDate() {
for (RaftKVTransaction tx : this.raft.openTransactions.values())
this.checkRebasableAndCommittableUpToDate(tx);
return true;
}
// Assertion check
boolean checkRebasableAndCommittableUpToDate(RaftKVTransaction tx) {
// A rebasable transactions should be fully rebased
assert !tx.isRebasable() || tx.getBaseIndex() == this.raft.getLastLogIndex() : "rebasable check failed for " + tx;
// A committable transaction should be marked as such
if (!tx.isCommittable()) {
try {
assert !this.checkCommittable(tx);
} catch (KVTransactionException e) {
// ok - it's not committable because it's broken
}
}
return true;
}
/**
* Determine whether the given log entry may be applied to the state machine.
* This method can assume that the log entry is already committed.
*
* @param logEntry log entry to apply
*/
final boolean mayApplyLogEntry(LogEntry logEntry) {
assert Thread.holdsLock(this.raft);
// Are we running out of memory, or keeping around too many log entries? If so, go ahead no matter what the subclass says.
final long logEntryMemoryUsage = this.raft.getUnappliedLogMemoryUsage();
if (logEntryMemoryUsage > this.raft.maxUnappliedLogMemory || this.raft.raftLog.size() > this.raft.maxUnappliedLogEntries) {
if (this.log.isTraceEnabled()) {
this.trace("allowing log entry " + logEntry + " to be applied because memory usage "
+ logEntryMemoryUsage + " > " + this.raft.maxUnappliedLogMemory + " and/or log length "
+ this.raft.raftLog.size() + " > " + this.raft.maxUnappliedLogEntries);
}
return true;
}
// Check with subclass
return this.roleMayApplyLogEntry(logEntry);
}
/**
* Role-specific hook to determine whether the given log entry should be applied to the state machine.
* This method can assume that the log entry is already committed.
*
* @param logEntry log entry to apply
*/
boolean roleMayApplyLogEntry(LogEntry logEntry) {
return true;
}
/**
* Trigger any key watches for changes in log entries committed since the last time we checked.
*
*
* This should be invoked:
*
* - After advancing the commitIndex
* - After resetting the state machine
* - After installing a snapshot
*
*/
void triggerKeyWatches() {
// Sanity check
assert Thread.holdsLock(this.raft);
assert this.raft.commitIndex >= this.raft.lastAppliedIndex;
assert this.raft.commitIndex <= this.raft.lastAppliedIndex + this.raft.raftLog.size();
assert this.raft.keyWatchIndex <= this.raft.commitIndex;
// If nobody is watching, don't bother
if (this.raft.keyWatchTracker == null)
return;
// If we have recevied a snapshot install, we may not be able to tell which keys have changed since last notification;
// in that case, trigger all key watches; otherwise, trigger the keys affected by newly committed log entries
if (this.raft.keyWatchIndex < this.raft.lastAppliedIndex) {
this.raft.keyWatchTracker.triggerAll();
this.raft.keyWatchIndex = this.raft.commitIndex;
} else {
while (this.raft.keyWatchIndex < this.raft.commitIndex)
this.raft.keyWatchTracker.trigger(this.raft.getLogEntryAtIndex(++this.raft.keyWatchIndex).getWrites());
}
}
// Transactions
/**
* Handle the situation where a {@link Consistency#LINEARIZABLE} transaction in state {@link TxState#EXECUTING}
* transitions from read-write to read-only.
*/
void handleLinearizableReadOnlyChange(RaftKVTransaction tx) {
// Sanity check
assert Thread.holdsLock(this.raft);
assert tx.getState().equals(TxState.EXECUTING);
assert tx.getConsistency().equals(Consistency.LINEARIZABLE);
assert tx.isReadOnly();
assert !tx.hasCommitInfo();
assert tx.isRebasable();
assert !tx.isCommittable();
assert this.checkRebasableAndCommittableUpToDate(tx);
}
/**
* Check a transaction that is ready to be committed (in the {@link TxState#COMMIT_READY} state).
*
*
* This should be invoked:
*
* - After changing roles
* - After a transaction has entered the {@link TxState#COMMIT_READY} state
* - After the leader is newly known (in {@link FollowerRole})
* - After the leader's output queue goes from non-empty to empty (in {@link FollowerRole})
* - After the leader's {@code commitIndex} has advanced, in case a config change transaction
* is waiting on a previous config change transaction (in {@link LeaderRole})
*
*
* @param tx the transaction
* @throws KVTransactionException if an error occurs
*/
final void checkReadyTransaction(RaftKVTransaction tx) {
// Sanity check
assert Thread.holdsLock(this.raft);
assert tx.getState().equals(TxState.COMMIT_READY);
// If transaction already has a commit term & index, proceed to COMMIT_WAITING
if (tx.hasCommitInfo()) {
this.advanceReadyTransaction(tx);
return;
}
// Requires leader communication to acquire commit term+index - let subclass handle it
assert !tx.isCommittable();
assert tx.getConsistency().equals(Consistency.LINEARIZABLE);
this.checkReadyTransactionNeedingCommitInfo(tx);
}
/**
* Handle a linearizable transaction that is ready to be committed (in the {@link TxState#COMMIT_READY} state) but
* does not yet have a commit term & index and therefore requires communication with the leader.
*
* @param tx the transaction
* @throws KVTransactionException if an error occurs
*/
void checkReadyTransactionNeedingCommitInfo(RaftKVTransaction tx) {
// Sanity check
assert Thread.holdsLock(this.raft);
assert tx.getState().equals(TxState.COMMIT_READY);
assert tx.getConsistency().equals(Consistency.LINEARIZABLE);
assert !tx.hasCommitInfo();
assert !tx.isCommittable();
assert this.checkRebasableAndCommittableUpToDate(tx);
}
/**
* Advance a transaction from the {@link TxState#COMMIT_READY} state to the {@link TxState#COMMIT_WAITING} state.
*
* @param tx the transaction
* @param commitTerm term of log entry that must be committed before the transaction may succeed
* @param commitIndex index of log entry that must be committed before the transaction may succeed
* @param commitLeaderLeaseTimeout if not null, minimum leader lease timeout we must see before commit may succeed
*/
final void advanceReadyTransactionWithCommitInfo(RaftKVTransaction tx,
long commitTerm, long commitIndex, Timestamp commitLeaderLeaseTimeout) {
// Sanity check
assert Thread.holdsLock(this.raft);
assert tx.getState().equals(TxState.COMMIT_READY);
assert !tx.hasCommitInfo();
// Set commit term & index
tx.setCommitInfo(commitTerm, commitIndex, commitLeaderLeaseTimeout);
// Advance to COMMIT_WAITING
this.advanceReadyTransaction(tx);
}
/**
* Advance a transaction from the {@link TxState#COMMIT_READY} state to the {@link TxState#COMMIT_WAITING} state.
*
*
* This assumes the commit info is already set.
*
* @param tx the transaction
*/
final void advanceReadyTransaction(RaftKVTransaction tx) {
// Sanity check
assert Thread.holdsLock(this.raft);
assert tx.getState().equals(TxState.COMMIT_READY);
assert tx.hasCommitInfo();
// Update state
if (this.log.isTraceEnabled())
this.trace("advancing " + tx + " to " + TxState.COMMIT_WAITING);
tx.setState(TxState.COMMIT_WAITING);
tx.setNoLongerRebasable();
this.checkCommittable(tx);
// Check this transaction to see if it can be committed
new CheckWaitingTransactionService(this, tx).run();
}
/**
* Check a transaction waiting for its log entry to be committed (in the {@link TxState#COMMIT_WAITING} state).
*
*
* This should be invoked:
*
* - After changing roles
* - After a transaction has entered the {@link TxState#COMMIT_WAITING} state
* - After advancing my {@code commitIndex} (as leader or follower)
* - After receiving an updated {@linkplain AppendResponse#getLeaderLeaseTimeout leader lease timeout}
* (in {@link FollowerRole})
*
*
* @param tx the transaction
* @throws KVTransactionException if an error occurs
*/
final void checkWaitingTransaction(RaftKVTransaction tx) {
// Sanity check
assert Thread.holdsLock(this.raft);
assert tx.getConsistency().isGuaranteesUpToDateReads();
// Is transaction committable?
if (!this.checkCommittable(tx))
return;
// Is there a required minimum leader lease timeout associated with the transaction? If so, we must wait for it.
final Timestamp commitLeaderLeaseTimeout = tx.getCommitLeaderLeaseTimeout();
if (commitLeaderLeaseTimeout != null && !this.isLeaderLeaseActiveAt(commitLeaderLeaseTimeout)) {
if (this.log.isTraceEnabled())
this.trace("committable " + tx + " must wait for leader lease timeout " + commitLeaderLeaseTimeout);
return;
}
// Allow transaction commit to complete
if (this.log.isTraceEnabled())
this.trace("commit successful for " + tx);
this.raft.succeed(tx);
}
/**
* Detect newly-committable transactions.
*
*
* This should be invoked after advancing my {@code commitIndex} (as leader or follower).
*
* @param tx the transaction
* @throws KVTransactionException if an error occurs
*/
void checkCommittables() {
// Sanity check
assert Thread.holdsLock(this.raft);
// Check which transactions are now committable
for (RaftKVTransaction tx : new ArrayList<>(this.raft.openTransactions.values())) {
try {
this.checkCommittable(tx);
} catch (KVTransactionException e) {
this.raft.fail(tx, e);
} catch (Exception | Error e) {
this.raft.error("error checking committable for transaction " + tx, e);
this.raft.fail(tx, new KVTransactionException(tx, e));
}
}
}
/**
* Determine if a transction has become committable, and mark it so if so.
*
*
* This should be invoked after advancing my {@code commitIndex} (as leader or follower), after setting
* the commit info for a transaction, or after rebasing a transaction that has commit info already.
*
*
* Note: "committable" means ready to commit except any required wait for {@code tx.commitLeaderLeaseTimeout}.
* In particular, the commit term+index is known, the corresponding log entry has been committed, and if rebasable
* the transaction is rebased up through the commit term+index.
*
* @param tx the transaction
* @throws KVTransactionException if an error occurs
*/
boolean checkCommittable(RaftKVTransaction tx) {
// Sanity check
assert Thread.holdsLock(this.raft);
// Already checked?
if (tx.isCommittable())
return true;
// Has the transaction's commit info been determined yet?
final long commitIndex = tx.getCommitIndex();
final long commitTerm = tx.getCommitTerm();
if (commitIndex == 0)
return false;
// Has the transaction's commit log entry been added yet?
final long lastIndex = this.raft.getLastLogIndex();
if (commitIndex > lastIndex)
return false;
// Compare commit term to the actual term of the commit log entry
final long commitIndexActualTerm = this.raft.getLogTermAtIndexIfKnown(commitIndex);
if (commitIndexActualTerm == 0) {
// The commit log entry has already been applied to the state machine and its term forgotten.
// This can happen if we lose contact and by the time we're back the log entry has
// already been applied to the state machine on some leader and that leader sent
// us an InstallSnapshot message. We don't know whether it actually got committed
// or not, so the transaction must be retried.
throw new RetryTransactionException(tx, "commit index " + commitIndex
+ " < last applied log index " + this.raft.lastAppliedIndex);
}
// Verify the term of the committed log entry; if not what we expect, the log entry was overwritten by a new leader
if (commitTerm != commitIndexActualTerm) {
throw new RetryTransactionException(tx, "leader was deposed during commit and transaction's commit log entry "
+ commitIndex + "t" + commitTerm + " overwritten by " + commitIndex + "t" + commitIndexActualTerm);
}
// Has the transaction's commit log entry been committed yet?
if (commitIndex > this.raft.commitIndex)
return false;
// If transaction is rebasable, it must be rebased at least up through its commit index
if (tx.isRebasable() && tx.getBaseIndex() < commitIndex)
return false;
// The transaction's commit log entry is committed, so mark the transaction as committable
if (this.log.isTraceEnabled())
this.trace(tx + " is now committable: " + this.raft.commitIndex + " >= " + commitIndex + "t" + commitTerm);
tx.setCommittable();
if (tx.isRebasable())
tx.setNoLongerRebasable();
return true;
}
/**
* Rebase all rebasable transactions up to through the last log entry.
*
*
* We only rebase {@link Consistency#LINEARIZABLE} transactions that are either non-mutating or have not
* yet had a {@link CommitRequest} sent to the leader.
*
*
* This should be invoked after appending a new Raft log entry.
*
* @param tx the transaction
* @throws KVTransactionException if an error occurs
*/
void rebaseTransactions() {
// Sanity check
assert Thread.holdsLock(this.raft);
// Rebase all rebasable transactions
for (RaftKVTransaction tx : new ArrayList<>(this.raft.openTransactions.values())) {
if (!tx.isRebasable())
continue;
try {
this.rebaseTransaction(tx);
} catch (KVTransactionException e) {
this.raft.fail(tx, e);
} catch (Exception | Error e) {
this.raft.error("error rebasing transaction " + tx, e);
this.raft.fail(tx, new KVTransactionException(tx, e));
}
}
}
/**
* Rebase the given transaction so that its base log entry is the last log entry or its commit log entry,
* whichever is lower.
*
*
* This should be invoked for each {@linkplain RaftKVTransaction#isRebasable rebasable} transaction
* after appending a new log entry.
*
*
* This method assumes that the given transaction is {@linkplain RaftKVTransaction#isRebasable rebasable}.
*
* @param tx the transaction
* @throws KVTransactionException if an error occurs
*/
private void rebaseTransaction(RaftKVTransaction tx) {
// Sanity check
assert Thread.holdsLock(this.raft);
assert tx.isRebasable();
assert tx.getFailure() == null;
assert tx.getBaseIndex() >= this.raft.lastAppliedIndex;
assert !tx.hasCommitInfo() || tx.getCommitIndex() > tx.getBaseIndex();
assert !tx.hasCommitInfo() || !tx.addsLogEntry();
// Anything to do?
long baseIndex = tx.getBaseIndex();
final long lastIndex = this.raft.getLastLogIndex();
if (baseIndex == lastIndex)
return;
// Lock the mutable view so the rebase appears to happen instantaneously to any threads viewing the transaction
synchronized (tx.view) {
// Check for conflicts between transaction reads and newly committed log entries
while (baseIndex < lastIndex) {
// Check for conflicts
final LogEntry logEntry = this.raft.getLogEntryAtIndex(++baseIndex);
if (tx.view.getReads().isConflict(logEntry.getWrites())) {
if (this.log.isDebugEnabled())
this.debug("cannot rebase " + tx + " past " + logEntry + " due to conflicts, failing");
if (this.raft.dumpConflicts)
this.dumpConflicts(tx.view.getReads(), logEntry, "local txId=" + tx.txId);
throw new RetryTransactionException(tx, "writes of committed transaction at index " + baseIndex
+ " conflict with transaction reads from transaction base index " + tx.getBaseIndex());
}
// If we reach the transaction's commit log entry (if any), we can stop
if (baseIndex == tx.getCommitIndex()) {
tx.setNoLongerRebasable();
break;
}
}
// Update transaction
final long baseTerm = this.raft.getLogTermAtIndex(baseIndex);
if (this.log.isDebugEnabled()) {
this.debug("rebased " + tx + " from " + tx.getBaseIndex() + "t" + tx.getBaseTerm()
+ " -> " + baseIndex + "t" + baseTerm);
}
switch (tx.getState()) {
case EXECUTING:
assert !tx.hasCommitInfo() || tx.isReadOnly();
final MostRecentView view = new MostRecentView(this.raft, baseIndex);
assert view.getTerm() == baseTerm;
assert view.getIndex() == baseIndex;
tx.rebase(baseTerm, baseIndex, view.getView().getKVStore(), view.getSnapshot());
break;
case COMMIT_READY:
tx.rebase(baseTerm, baseIndex);
break;
case COMMIT_WAITING:
tx.rebase(baseTerm, baseIndex);
this.checkWaitingTransaction(tx); // transaction might have become committable
break;
default:
throw new RuntimeException("internal error");
}
}
// Check whether transaction has become committable
if (baseIndex == tx.getCommitIndex())
this.checkCommittable(tx);
}
void dumpConflicts(Reads reads, LogEntry logEntry, String description) {
final StringBuilder buf = new StringBuilder();
buf.append(description + " failing due to conflicts with " + logEntry + ":");
for (String conflict : reads.getConflicts(logEntry.getWrites()))
buf.append("\n ").append(conflict);
this.info(buf.toString());
}
/**
* Get the leader's lease timeout, if known.
*
* @return leader lease timeout, or null if unknown
*/
Timestamp getLeaderLeaseTimeout() {
return null;
}
/**
* Determine whether the leader's lease timeout extends past the current time, that is, it is known that if
* the current leader is deposed by a new leader, then that deposition must occur after now.
*
* @return true if it is known that no other leader can possibly have been elected at the current time, otherwise false
*/
protected boolean isLeaderLeaseActiveNow() {
return this.isLeaderLeaseActiveAt(new Timestamp());
}
/**
* Determine whether the leader's lease timeout extends past the given time, that is, it is known that if
* the current leader is deposed by a new leader, then that deposition must occur after the given time.
*
* @param time leader timestamp
* @return true if it is known that no other leader can possibly have been elected at the given time, otherwise false
*/
protected boolean isLeaderLeaseActiveAt(Timestamp time) {
final Timestamp leaderLeaseTimeout = this.getLeaderLeaseTimeout();
return leaderLeaseTimeout != null && leaderLeaseTimeout.compareTo(time) > 0;
}
/**
* Perform any role-specific transaction cleanups.
*
*
* Invoked either when transaction is completed OR this role is being shutdown.
*
*
* Subclasses should invoke this method if overriden.
*
* @param tx the transaction
*/
void cleanupForTransaction(RaftKVTransaction tx) {
assert Thread.holdsLock(this.raft);
}
// Messages
// This is a package access version of "implements MessageSwitch"
abstract void caseAppendRequest(AppendRequest msg, NewLogEntry newLogEntry);
abstract void caseAppendResponse(AppendResponse msg);
abstract void caseCommitRequest(CommitRequest msg, NewLogEntry newLogEntry);
abstract void caseCommitResponse(CommitResponse msg);
abstract void caseGrantVote(GrantVote msg);
abstract void caseInstallSnapshot(InstallSnapshot msg);
abstract void caseRequestVote(RequestVote msg);
void casePingRequest(PingRequest msg) {
assert Thread.holdsLock(this.raft);
final int responseClusterId = this.raft.clusterId != 0 ? this.raft.clusterId : msg.getClusterId();
this.raft.sendMessage(new PingResponse(responseClusterId,
this.raft.identity, msg.getSenderId(), this.raft.currentTerm, msg.getTimestamp()));
}
void casePingResponse(PingResponse msg) {
assert Thread.holdsLock(this.raft);
// ignore by default
}
boolean mayAdvanceCurrentTerm(Message msg) {
return true;
}
void failUnexpectedMessage(Message msg) {
this.warn("rec'd unexpected message " + msg + " while in role " + this + "; ignoring");
}
// Debug
abstract boolean checkState();
void checkTransaction(RaftKVTransaction tx) {
this.checkRebasableAndCommittableUpToDate(tx);
}
// Logging
void trace(String msg, Throwable t) {
this.raft.trace(msg, t);
}
void trace(String msg) {
this.raft.trace(msg);
}
void debug(String msg, Throwable t) {
this.raft.debug(msg, t);
}
void debug(String msg) {
this.raft.debug(msg);
}
void info(String msg, Throwable t) {
this.raft.info(msg, t);
}
void info(String msg) {
this.raft.info(msg);
}
void warn(String msg, Throwable t) {
this.raft.warn(msg, t);
}
void warn(String msg) {
this.raft.warn(msg);
}
void error(String msg, Throwable t) {
this.raft.error(msg, t);
}
void error(String msg) {
this.raft.error(msg);
}
// Object
@Override
public abstract String toString();
String toStringPrefix() {
assert Thread.holdsLock(this.raft);
return this.getClass().getSimpleName()
+ "[term=" + this.raft.currentTerm
+ ",applied=" + this.raft.lastAppliedIndex + "t" + this.raft.lastAppliedTerm
+ ",commit=" + this.raft.commitIndex
+ ",log=" + this.raft.raftLog
+ "]";
}
}