package lsr.paxos.storage;
import static lsr.common.ProcessDescriptor.processDescriptor;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Deque;
import lsr.paxos.UnBatcher;
import lsr.paxos.replica.ClientBatchID;
import lsr.paxos.replica.ClientBatchManager.FwdBatchRetransmitter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Contains data related with one consensus instance.
*/
public class ConsensusInstance implements Serializable {
private static final long serialVersionUID = 1L;
protected final int id;
protected int view;
protected byte[] value;
protected LogEntryState state;
protected transient BitSet accepts = new BitSet();
// For indirect consensus:
protected transient boolean decidable = false;
protected transient FwdBatchRetransmitter fbr = null;
/**
* Represents possible states of consensus instance.
*/
public enum LogEntryState {
/**
* Represents the empty consensus state. There is no information about
* current view nor value.
*/
UNKNOWN,
/**
* The consensus in this state received the <code>PROPOSE</code> message
* from the leader but hasn't received the majority of the
* <code>ACCEPT</code> messages. In this state there is some view and
* value specified, but they can be changed later.
*/
KNOWN,
/**
* Represents state when {@link lsr.paxos.Learner} received majority of
* <code>ACCEPT</code> message. In this state the view and value of
* consensus instance cannot be changed.
*/
DECIDED
}
/**
* Initializes new instance of consensus with all value specified.
*
* @param id - the id of instance to create
* @param state - the state of consensus
* @param view - the view of last message in this consensus
* @param value - the value accepted or decided in this instance
*/
public ConsensusInstance(int id, LogEntryState state, int view, byte[] value) {
this.id = id;
this.state = state;
this.view = view;
this.value = value;
onValueChange();
assertInvariant();
}
/**
* Initializes new empty instance of consensus. The initial state is set to
* <code>UNKNOWN</code>, view to <code>-1</code> and value to
* <code>null</code>.
*
* @param id the id of instance to create
*/
public ConsensusInstance(int id) {
this(id, LogEntryState.UNKNOWN, -1, null);
}
/**
* Initializes new instance of consensus from input stream. The input stream
* should contain serialized instance created by <code>toByteArray()</code>
* or <code>write(ByteBuffer)</code> method.
*
* @param input - the input stream containing serialized consensus instance
* @throws IOException the stream has been closed and the contained input
* stream does not support reading after close, or another I/O
* error occurs
* @see #toByteArray()
* @see #write(ByteBuffer)
*/
public ConsensusInstance(DataInputStream input) throws IOException {
this.id = input.readInt();
this.view = input.readInt();
this.state = LogEntryState.values()[input.readInt()];
int size = input.readInt();
if (size == -1) {
value = null;
} else {
value = new byte[size];
input.readFully(value);
}
onValueChange();
assertInvariant();
}
private void assertInvariant() {
// If value is non null, the state must be either Decided or Known.
// If value is null, it must be unknown
assert value == null ^ state != LogEntryState.UNKNOWN : "Invalid state. Value=" + value +
": " + toString();
}
/**
* Gets the number of the consensus instance. Different instances should
* have different id's.
*
* @return id of instance
*/
public int getId() {
return id;
}
/**
* Changes the view to the newest one. It cannot be changed to value less
* than current view, and shouldn't be changed if the consensus is already
* in <code>Decided</code> state.
*
* Clears the accepts if the new view is higher than the current one.
*
* @param view - the new view value
*/
public void setView(int view) {
assert this.view <= view : "Cannot set smaller view.";
assert state != LogEntryState.DECIDED || view == this.view;
if (this.view < view) {
accepts.clear();
this.view = view;
}
}
/**
* Gets the current view of this instance. The view of instance is
* represented by the view of last message. If the current state of
* consensus is decided, then view should not be changed.
*
* @return the view number of this instance
*/
public int getView() {
return view;
}
/**
* Sets new value holding by this instance. Each value has view in which it
* is valid, so it has to be set here also.
*
* @param view - the view number in which value is valid
* @param value - the value which was accepted by this instance
*/
protected void setValue(int view, byte[] value) {
assert value != null : "value cannot be null. View: " + view;
assert state != LogEntryState.DECIDED
|| Arrays.equals(this.value, value) : view + " " + value + " " + this;
assert state != LogEntryState.KNOWN
|| view != this.view
|| Arrays.equals(this.value, value) : view + " " + value + " " + this;
if (state == LogEntryState.UNKNOWN) {
state = LogEntryState.KNOWN;
}
setView(view);
this.value = value;
onValueChange();
}
/**
* Returns the value holding by this consensus. It represents last value
* which was accepted by <code>Acceptor</code>.
*
* @return the current value of this instance
*/
public byte[] getValue() {
return value;
}
/**
* Gets the current state of this instance. When the state is set to
* <code>DECIDED</code> no values should be changed.
*
* @return current state of consensus instance
*/
public LogEntryState getState() {
return state;
}
/**
* Gets the set of replicas from which we get the <code>ACCEPT</code>
* message from the current <code>view</code>.
*
* @return id's of replicas
*/
public BitSet getAccepts() {
return accepts;
}
/** Returns if the instances is accepted by the majority */
public boolean isMajority() {
return accepts.cardinality() >= processDescriptor.majority;
}
/**
* Changes the current state of this instance to <code>DECIDED</code>. This
* instance cannot be changed so <code>accepts</code> value will be set to
* <code>null</code>.
*
* @see #getAccepts()
*/
public void setDecided() {
assert value != null;
state = LogEntryState.DECIDED;
accepts = null;
assertInvariant();
}
/**
* Serializes and writes this consensus instance to specified byte buffer.
* Specified byte buffer requires at least <code>byteSize()</code> remaining
* size.
*
* @param byteBuffer - the buffer where serialized consensus instance will
* be written
* @see #byteSize()
*/
public void write(ByteBuffer byteBuffer) {
byteBuffer.putInt(id);
byteBuffer.putInt(view);
byteBuffer.putInt(state.ordinal());
if (value == null) {
byteBuffer.putInt(-1);
} else {
byteBuffer.putInt(value.length);
byteBuffer.put(value);
}
}
/**
* Returns size of serialized instance in bytes. This value is equal to
* length of array returned by <code>toByteArray()</code> method and number
* of bytes written to <code>ByteBuffer</code> using
* <code>write(ByteBuffer)</code> method.
*
* @return size of serialized instance
*/
public int byteSize() {
int size = (value == null ? 0 : value.length) + 4 /* length of array */;
size += 3 * 4 /* ID, view and state */;
return size;
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
result = prime * result + ((state == null) ? 0 : state.hashCode());
result = prime * result + Arrays.hashCode(value);
result = prime * result + view;
return result;
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ConsensusInstance other = (ConsensusInstance) obj;
if (id != other.id) {
return false;
}
if (state == null) {
if (other.state != null) {
return false;
}
} else if (!state.equals(other.state)) {
return false;
}
if (!Arrays.equals(value, other.value)) {
return false;
}
if (view != other.view) {
return false;
}
return true;
}
public String toString() {
return "(" + id + ", " + state + ", view=" + view + ", value=" + value + ")";
}
/** Called when received a higher view Accept */
public void reset() {
accepts.clear();
state = LogEntryState.UNKNOWN;
value = null;
assertInvariant();
}
/**
* Ignores any update with a view lower than the local one.
*
* @param newView
* @param newValue
*/
public void updateStateFromKnown(int newView, byte[] newValue) {
// Ignore any state update from an older view.
if (newView < view) {
return;
}
switch (state) {
case DECIDED:
/*
* This can happen when the new leader re-proposes an instance
* that was decided by some processes on a previous view.
*
* The value must be the same as the local value.
*/
assert Arrays.equals(newValue, value) : "Values don't match. New view: " + newView +
", local: " + this + ", newValue: " +
Arrays.toString(newValue) + ", old: " +
Arrays.toString(value);
break;
case KNOWN:
// The message is for a higher view or same view and same value.
// State remains in known
assert newView != view || Arrays.equals(newValue, value) : "Values don't match. newView: " +
newView +
", local: " +
this;
setValue(newView, newValue);
break;
case UNKNOWN:
setValue(newView, newValue);
break;
default:
throw new RuntimeException("Unknown instance state");
}
}
/**
* Update the local state from an already decided instance. This differs
* from {@link #updateStateFromKnown(int, byte[])} in that it will accept
* messages from lower views.
*
* Used during catchup or view change, when the replica may receive messages
* from lower views that are decided.
*
* State is set to known, as the instance is decided from other class
*
* This method does not check if args are valid, as this is unnecessary in
* this case.
*
* @param newView
* @param newValue
*/
public void updateStateFromDecision(int newView, byte[] newValue) {
assert newValue != null;
if (state == LogEntryState.DECIDED) {
logger.error("Updating a decided instance from a catchup message: {}", this);
// The value must be the same as the local value. No change.
assert Arrays.equals(newValue, value) : "Values don't match. New view: " + newView +
", local: " + this;
return;
} else {
this.view = newView;
this.value = newValue;
this.state = LogEntryState.KNOWN;
onValueChange();
}
}
protected final void onValueChange() {
if (value == null)
return;
if (processDescriptor.indirectConsensus) {
for (ClientBatchID cbid : getClientBatchIds()) {
ClientBatchStore.instance.associateWithInstance(cbid);
}
}
}
/**
* If the instance is ready to be decided, but misses batch values it is
* decidable. Catch-Up will not bother for such instances.
*/
public void setDecidable(boolean decidable) {
this.decidable = decidable;
}
/**
* Returns if the instance is decided or ready to be decided.
*
* FOR CATCH-UP PURPOSES ONLY
*/
public boolean isDecidable() {
return LogEntryState.DECIDED.equals(state) || decidable;
}
protected transient Deque<ClientBatchID> cbids = null;
/**
* Returns the batch IDs contained in the request, unpacking the request if
* necessary
*
* DO NOT MODIFY THE RESULT
*/
public Deque<ClientBatchID> getClientBatchIds() {
assert processDescriptor.indirectConsensus;
if (cbids == null)
cbids = UnBatcher.unpackCBID(value);
return cbids;
}
/**
* If it has been necessary to unpack the value earlier, this allows not to
* waste the effort of unpacking
*/
public void setClientBatchIds(Deque<ClientBatchID> cbids) {
this.cbids = cbids;
}
/** If there is a task fetching missing batch values, the task is stopped */
public void stopFwdBatchForwarder() {
if (fbr != null)
ClientBatchStore.instance.getClientBatchManager().removeTask(fbr);
}
/** Sets a task for fetching the batch values */
public void setFwdBatchForwarder(FwdBatchRetransmitter fbr) {
this.fbr = fbr;
}
private final static Logger logger = LoggerFactory.getLogger(ConsensusInstance.class);
}