package lsr.paxos.storage; import static lsr.common.ProcessDescriptor.processDescriptor; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; import java.util.Vector; import lsr.paxos.UnBatcher; import lsr.paxos.replica.ClientBatchID; import lsr.paxos.storage.ConsensusInstance.LogEntryState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A basic class implementing features needed by Paxos log. This class is not * using stable storage, that means all records are kept in RAM memory * exclusively! */ public class Log { /** Structure containing all kept instances */ protected final TreeMap<Integer, ConsensusInstance> instances; // This field is read from other threads (eg., ActiveFailureDetector), // therefore must be made volatile to ensure visibility of changes /** ID of next instance, that is highest instanceId + 1 */ protected volatile int nextId = 0; /** Lowest still held in memory instance number */ protected Integer lowestAvailable = 0; /** List of objects to be informed about log changes */ private List<LogListener> listeners = new Vector<LogListener>(); /** * Creates new instance of empty <code>Log</code>. */ public Log() { instances = new TreeMap<Integer, ConsensusInstance>(); } /** Returns read-only access to the log */ public SortedMap<Integer, ConsensusInstance> getInstanceMap() { return Collections.unmodifiableSortedMap(instances); } /** Returns, creating if needed, instance with provided ID */ public ConsensusInstance getInstance(int instanceId) { int oldNextId = nextId; while (nextId <= instanceId) { instances.put(nextId, createInstance()); nextId++; } if (oldNextId != nextId) { sizeChanged(); } return instances.get(instanceId); } /** * Adds a new instance at the end of the log. * * @param view - the view of new consensus instance * @param value - the value of new consensus instance * @return new consensus log */ public ConsensusInstance append(int view, byte[] value) { ConsensusInstance instance = createInstance(view, value); instances.put(nextId, instance); nextId++; sizeChanged(); return instance; } /** * Returns the id of next consensus instance. The id of highest instance * stored in the log is equal to <code>getNextId() - 1</code>. * * @return the id of next consensus instance */ public int getNextId() { return nextId; } /** * Returns the id of lowest available instance (consensus instance with the * smallest id). All previous instances were truncated. The instance can be * of any state. * * In some cases there actually are some instances below this point, but the * lower numbers are not contiguous. (See {@link #clearUndecidedBelow}) * * @return the id of lowest available instance */ public int getLowestAvailableId() { return lowestAvailable; } /** * Removes instances with ID's strictly smaller than a given one. After * truncating the log, {@link #lowestAvailable} is updated. * * @param instanceId - the id of consensus instance. * @return removed instances */ public void truncateBelow(int instanceId) { assert instanceId >= lowestAvailable : "Cannot truncate below lower available."; lowestAvailable = instanceId; nextId = Math.max(nextId, lowestAvailable); ArrayList<ConsensusInstance> removed = new ArrayList<ConsensusInstance>(); if (instances.isEmpty()) { return; } if (instanceId >= nextId) { removed.addAll(instances.values()); instances.clear(); if (processDescriptor.indirectConsensus) clearBatches(removed); return; } while (instances.firstKey() < instanceId) { removed.add((instances.pollFirstEntry().getValue())); } logger.debug("Truncated log below: {}", instanceId); if (processDescriptor.indirectConsensus) clearBatches(removed); } /** * Removes all undecided instances below given point. All instances with id * lower than <code>instanceId</code> and not decided will be removed from * the log. * * @param instanceId - the id of consensus instance * @return removed instances */ public void clearUndecidedBelow(int instanceId) { if (instances.size() == 0) { return; } lowestAvailable = instanceId; nextId = Math.max(nextId, lowestAvailable); ArrayList<ConsensusInstance> removed = new ArrayList<ConsensusInstance>(); int first = instances.firstKey(); for (int i = first; i < instanceId; i++) { ConsensusInstance instance = instances.get(i); if (instance != null && instance.getState() != LogEntryState.DECIDED) { removed.add(instances.remove(i)); } } if (processDescriptor.indirectConsensus) clearBatches(removed); } private void clearBatches(ArrayList<ConsensusInstance> removed) { HashSet<ClientBatchID> cbids = new HashSet<ClientBatchID>(); for (ConsensusInstance ci : removed) { if (ci.getValue() != null) cbids.addAll(UnBatcher.unpackCBID(ci.getValue())); ci.stopFwdBatchForwarder(); } for (ConsensusInstance ci : instances.values()) { if (ci.getValue() != null) cbids.removeAll(UnBatcher.unpackCBID(ci.getValue())); } ClientBatchStore.instance.removeBatches(cbids); } /** * Registers specified listener. This listener will be called every time * this log has been changed. * * @param listener - the listener to register * @return true if the listener has been registered. */ public boolean addLogListener(LogListener listener) { return listeners.add(listener); } /** * Unregisters specified listener from the log. * * @param listener - the listener that will be removed * @return true if the listener was already registered */ public boolean removeLogListener(LogListener listener) { return listeners.remove(listener); } /** * Calls function on all objects, that should be informed on log size * change. */ protected void sizeChanged() { for (LogListener listener : listeners) { listener.logSizeChanged(instances.size()); } } /** * Returns approximate size of log from 'startId' (inclusive) to 'endId' * (exclusive) in bytes. * * That is, size of consensus instances in range <startId, endId). * * @return size of log in bytes */ public long byteSizeBetween(int startId, int endId) { int start = Math.max(startId, instances.firstKey()); int stop = Math.min(endId, nextId); long size = 0; ConsensusInstance current; for (int i = start; i < stop; ++i) { current = instances.get(i); if (current == null) { continue; } size += current.byteSize(); } return size; } /** * Creates new empty consensus instance. * * @return new consensus instance. */ protected ConsensusInstance createInstance() { return new ConsensusInstance(nextId); } /** * Creates new consensus instance with specified view and value. * * @param view - the view number * @param value - the value * @return new consensus instance */ protected ConsensusInstance createInstance(int view, byte[] value) { return new ConsensusInstance(nextId, LogEntryState.KNOWN, view, value); } private final static Logger logger = LoggerFactory.getLogger(Log.class); }