package org.corfudb.runtime.view.stream;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.corfudb.protocols.logprotocol.StreamCOWEntry;
import org.corfudb.protocols.wireprotocol.DataType;
import org.corfudb.protocols.wireprotocol.ILogData;
import org.corfudb.runtime.CorfuRuntime;
import org.corfudb.runtime.view.Address;
import org.corfudb.util.Utils;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
/** An abstract context stream view maintains contexts, which are used to
* implement copy-on-write entries.
*
* This implementation uses "contexts" to properly deal with copy-on-write
* streams. Every time a stream is copied, a new context is created which
* redirects requests to the source stream for the copy - each context
* contains its own queue and pointers. Implementers of fillReadQueue() and
* readAndUpdatePointers should be careful to use the id of the context,
* rather than that of the stream view itself.
*
* Created by mwei on 1/6/17.
*/
@Slf4j
public abstract class AbstractContextStreamView<T extends AbstractStreamContext>
implements IStreamView, AutoCloseable {
/**
* The ID of the stream.
*/
@Getter
final UUID ID;
/**
* The runtime the stream view was created with.
*/
final CorfuRuntime runtime;
/**
* An ordered set of stream contexts, which store information
* about a stream copied via copy-on-write entries. Streams which
* have never been copied have only a single context.
*/
final NavigableSet<T> streamContexts;
/** A function which creates a context, given the stream ID and max global.
*/
final BiFunction<UUID, Long, T> contextFactory;
/** The base context, which is always preserved. */
final T baseContext;
/** Create a new abstract context stream view.
*
* @param runtime The runtime.
* @param id The id of the stream.
* @param contextFactory A function which generates a context,
* given the stream id and a maximum global
* address.
*/
public AbstractContextStreamView(final CorfuRuntime runtime,
final UUID id,
final BiFunction<UUID, Long, T>
contextFactory) {
this.ID = id;
this.runtime = runtime;
this.streamContexts = new TreeSet<>();
this.contextFactory = contextFactory;
this.baseContext = contextFactory.apply(id, Address.MAX);
this.streamContexts.add(baseContext);
}
/**
* {@inheritDoc}
*/
public synchronized void reset() {
this.streamContexts.clear();
baseContext.reset();
this.streamContexts.add(baseContext);
}
/**
* {@inheritDoc}
*/
@Override
public synchronized void seek(long globalAddress) {
// pop any stream context which has a max address
// less than the global address
while (this.streamContexts.size() > 1) {
if (this.streamContexts.first().maxGlobalAddress <
globalAddress)
{
this.streamContexts.pollFirst();
}
}
// now request a seek on the context
this.streamContexts.first().seek(globalAddress);
}
/**
* {@inheritDoc}
*/
@Override
public void close() {}
/**
* {@inheritDoc}
*/
@Override
final public synchronized ILogData nextUpTo(final long maxGlobal) {
// Don't do anything if we've already exceeded the global
// pointer.
if (getCurrentContext().globalPointer > maxGlobal) {
return null;
}
// Pop the context if it has changed.
if (getCurrentContext().globalPointer >=
getCurrentContext().maxGlobalAddress) {
final T last = streamContexts.pollFirst();
log.trace("Completed context {}@{}, removing.",
last.id, last.maxGlobalAddress);
}
// Get the next entry from the underlying implementation.
final ILogData entry =
getNextEntry(getCurrentContext(), maxGlobal);
if (entry != null) {
// Update the pointer.
updatePointer(entry);
// Process the next entry, checking if the context has changed.
// If the context has changed, we read again, since this entry
// does not contain any data, and we need to follow the new
// context.
if (processEntryForContext(entry)) {
return nextUpTo(maxGlobal);
}
}
// Return the entry.
return entry;
}
/** {@inheritDoc}
*/
@Override
public final synchronized List<ILogData> remainingUpTo(long maxGlobal) {
// Pop the context if it has changed.
if (getCurrentContext().globalPointer >=
getCurrentContext().maxGlobalAddress) {
final T last = streamContexts.pollFirst();
log.trace("Completed context {}@{}, removing.",
last.id, last.maxGlobalAddress);
}
final List<ILogData> entries = getNextEntries(getCurrentContext(), maxGlobal,
this::doesEntryUpdateContext);
// Nothing read, nothing to process.
if (entries.size() == 0) {
// We've resolved up to maxGlobal, so remember it. (if it wasn't max)
if (maxGlobal != Address.MAX) {
getCurrentContext().globalPointer = maxGlobal;
}
return entries;
}
// Check if the last entry updates the context.
if (doesEntryUpdateContext(entries.get(entries.size() - 1)))
{
// The entry which updates the context must be the last one, so
// process it
processEntryForContext(entries.get(entries.size() - 1));
// Remove the entry which updates the context
entries.remove(entries.size() - 1);
// do a read again, which will consume the inner context
entries.addAll(remainingUpTo(maxGlobal));
// and now read again, in case the context returned, to make
// sure we get up to maxGlobal.
entries.addAll(remainingUpTo(maxGlobal));
}
// Otherwise update the pointer
if (maxGlobal != Address.MAX) {
getCurrentContext().globalPointer = maxGlobal;
} else {
updatePointer(entries.get(entries.size() - 1));
}
// And return the entries.
return entries;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasNext() {
return getHasNext(getCurrentContext());
}
/** Return whether calling getNextEntry() may return more
* entries, given the context.
* @param context The context to retrieve the next entry from.
* @return True, if getNextEntry() may return an entry.
* False otherwise.
*/
abstract protected boolean getHasNext(T context);
/** Retrieve the next entry in the stream, given the context.
*
* @param context The context to retrieve the next entry from.
* @param maxGlobal The maximum global address to read to.
* @return
*/
abstract protected ILogData getNextEntry(T context, long maxGlobal);
/** Retrieve the next entries in the stream, given the context.
*
* This function is designed to implement a bulk read. In a bulk read,
* one of the entries may cause the context to change - the implementation
* should check if the entry changes the context and stop reading
* if this occurs, returning the entry that caused contextCheckFn to return
* true.
*
* The default implementation simply calls getNextEntry.
*
* @param context The context to retrieve the next entry from.
* @param maxGlobal The maximum global address to read to.
* @param contextCheckFn A function which returns true if the entry changes the stream context.
* @return
*/
protected List<ILogData> getNextEntries(T context, long maxGlobal,
Function<ILogData, Boolean> contextCheckFn) {
final List<ILogData> dataList = new ArrayList<>();
ILogData thisData;
while ((thisData = getNextEntry(context, maxGlobal)) != null) {
// Add this read to the list of reads to return.
dataList.add(thisData);
// Update the pointer, because the underlying implementation
// will expect it to be updated when we call getNextEntry() again.
updatePointer(thisData);
// If this entry changes the context, don't continue reading.
if (contextCheckFn.apply(thisData)) {
break;
}
}
return dataList;
}
/** Check whether the given entry updates the context.
*
* @param data The entry to check.
* @return True, if the entry will update the context.
*/
protected boolean doesEntryUpdateContext(final ILogData data) {
return data.hasBackpointer(getCurrentContext().id) &&
data.getBackpointer(getCurrentContext().id)
.equals(Address.COW_BACKPOINTER);
}
/** Update the global pointer, given an entry.
*
* @param data The entry to use to update the pointer.
*/
protected void updatePointer(final ILogData data) {
// Update the global pointer, if it is data.
if (data.getType() == DataType.DATA) {
getCurrentContext().globalPointer =
data.getGlobalAddress();
}
}
/** Check if the given entry adds a new context, and update
* the global pointer.
*
* If it does, add it to the context stack. Otherwise,
* pop the context.
*
* It is important that this method be called in order, since
* it updates the global pointer and can change the global pointer.
*
* @param data The entry to process.
* @return True, if this entry adds a context.
*/
protected boolean processEntryForContext(final ILogData data) {
if (data != null) {
final Object payload = data.getPayload(runtime);
// If this is a COW entry, we update the context as well.
if (payload instanceof StreamCOWEntry) {
StreamCOWEntry ce = (StreamCOWEntry) payload;
pushNewContext(ce.getOriginalStream(),
ce.getFollowUntil());
return true;
}
}
return false;
}
/** Get the current context.
*
* Should never throw a NoSuchElement exception because streamContexts should
* always at least have one element.
*
* */
protected T getCurrentContext() {
return streamContexts.first();
}
/** Add a new context. */
protected void pushNewContext(UUID id, long maxGlobal) {
streamContexts.add(contextFactory.apply(id, maxGlobal));
}
protected void popContext() {
if (streamContexts.size() <= 1) {
throw new RuntimeException("Attempted to pop context with less than 1 context remaining!");
}
streamContexts.pollFirst();
}
@Override
public String toString() {
return Utils.toReadableID(baseContext.id) + "@" + getCurrentContext().globalPointer;
}
}