package org.corfudb.runtime.view.stream; import lombok.extern.slf4j.Slf4j; import org.corfudb.protocols.wireprotocol.ILogData; import org.corfudb.protocols.wireprotocol.TokenResponse; import org.corfudb.runtime.CorfuRuntime; import org.corfudb.runtime.exceptions.OverwriteException; import org.corfudb.runtime.view.Address; import javax.annotation.Nonnull; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** A view of a stream implemented with backpointers. * * In this implementation, all addresses are global (log) addresses. * * All method calls of this class are thread-safe. * * Created by mwei on 12/11/15. */ @Slf4j public class BackpointerStreamView extends AbstractQueuedStreamView { /** Create a new backpointer stream view. * * @param runtime The runtime to use for accessing the log. * @param streamID The ID of the stream to view. */ public BackpointerStreamView(final CorfuRuntime runtime, final UUID streamID) { super(runtime, streamID); } /** * {@inheritDoc} * * In the backpointer-based implementation, we loop forever trying to * write, and automatically retrying if we get overwritten (hole filled). */ @Override public long append(Object object, Function<TokenResponse, Boolean> acquisitionCallback, Function<TokenResponse, Boolean> deacquisitionCallback) { // First, we get a token from the sequencer. TokenResponse tokenResponse = runtime.getSequencerView() .nextToken(Collections.singleton(ID), 1); // We loop forever until we are interrupted, since we may have to // acquire an address several times until we are successful. while (true) { // Next, we call the acquisitionCallback, if present, informing // the client of the token that we acquired. if (acquisitionCallback != null) { if (!acquisitionCallback.apply(tokenResponse)) { // The client did not like our token, so we end here. // We'll leave the hole to be filled by the client or // someone else. log.debug("Acquisition rejected token={}", tokenResponse); return -1L; } } // Now, we do the actual write. We could get an overwrite // exception here - any other exception we should pass up // to the client. try { runtime.getAddressSpaceView() .write(tokenResponse, object); // The write completed successfully, so we return this // address to the client. return tokenResponse.getToken().getTokenValue(); } catch (OverwriteException oe) { log.trace("Overwrite occurred at {}", tokenResponse); // We got overwritten, so we call the deacquisition callback // to inform the client we didn't get the address. if (deacquisitionCallback != null) { if (!deacquisitionCallback.apply(tokenResponse)) { log.debug("Deacquisition requested abort"); return -1L; } } // Request a new token, informing the sequencer we were // overwritten. tokenResponse = runtime.getSequencerView() .nextToken(Collections.singleton(ID), 1); } } } /** {@inheritDoc} * * The backpointer version of remaining() calls nextUpTo() multiple times, * as it uses the default implementation in IStreamView. While this may * appear to be non-optimized, these reads will most likely hit in the * address space cache since the entries were read in order to resolve the * backpointers. * * */ @Override protected ILogData read(final long address) { return runtime.getAddressSpaceView().read(address); } @Nonnull @Override protected List<ILogData> readAll(@Nonnull List<Long> addresses) { Map<Long, ILogData> dataMap = runtime.getAddressSpaceView().read(addresses); return addresses.stream() .map(x -> dataMap.get(x)) .collect(Collectors.toList()); } /** * {@inheritDoc} * * In the backpointer based implementation, we indicate we may have * entries available if the read queue contains entries to read -or- * if the next token is greater than our log pointer. */ @Override public boolean getHasNext(QueuedStreamContext context) { return context.readQueue.isEmpty() || runtime.getSequencerView() .nextToken(Collections.singleton(context.id), 0).getToken().getTokenValue() > context.globalPointer; } /** * {@inheritDoc} */ @Override public void close() {} protected boolean fillFromResolved(final long maxGlobal, final QueuedStreamContext context) { // There's nothing to read if we're already past maxGlobal. if (maxGlobal < context.globalPointer) { return false; } // Get the subset of the resolved queue, which starts at // globalPointer and ends at maxAddress inclusive. NavigableSet<Long> resolvedSet = context.resolvedQueue.subSet(context.globalPointer, false, maxGlobal, true); // Put those elements in the read queue context.readQueue.addAll(resolvedSet); return !context.readQueue.isEmpty(); } /** * {@inheritDoc} */ @Override protected boolean fillReadQueue(final long maxGlobal, final QueuedStreamContext context) { log.trace("Read_Fill_Queue[{}] Max: {}, Current: {}, Resolved: {} - {}", this, maxGlobal, context.globalPointer, context.maxResolution, context.minResolution); // The maximum address we will fill to. final long maxAddress = Long.min(maxGlobal, context.maxGlobalAddress); // If the maximum address is less than the current pointer, // we return since there is nothing left to do. if (context.globalPointer > maxAddress) { return false; } // If everything is available in the resolved // queue, use it if (context.maxResolution > maxAddress && context.minResolution < context.globalPointer) { return fillFromResolved(maxGlobal, context); } Long latestTokenValue = null; // If the max has been resolved, use it. if (maxGlobal != Address.MAX) { latestTokenValue = context.resolvedQueue.ceiling(maxGlobal); } // If we don't have a larger token in resolved, or the request was for // a linearized read, fetch the token from the sequencer. if (latestTokenValue == null || maxGlobal == Address.MAX) { latestTokenValue = runtime.getSequencerView() .nextToken(Collections.singleton(context.id), 0) .getToken().getTokenValue(); } // If there is no infomation on the tail of the stream, return, there is nothing to do if (Address.nonAddress(latestTokenValue)) { // sanity check: // curretly, the only possible non-address return value for a token-query is Address.NON_EXIST if (latestTokenValue != Address.NON_EXIST) log.warn("TOKEN[{}] unexpected return value", latestTokenValue); return false; } // If everything is available in the resolved // queue, use it if (context.maxResolution > latestTokenValue && context.minResolution < context.globalPointer) { return fillFromResolved(latestTokenValue, context); } // Now we start traversing backpointers, if they are available. We // start at the latest token and go backward, until we reach the // log pointer. For each address which is less than // maxGlobalAddress, we insert it into the read queue. long currentRead = latestTokenValue; while (currentRead > context.globalPointer && Address.isAddress(currentRead)) { // We've somehow reached a read we already know about. if (context.readQueue.contains(currentRead)) { break; } log.trace("Read_Fill_Queue[{}] Read {}", this, currentRead); // Read the entry in question. ILogData currentEntry = runtime.getAddressSpaceView().read(currentRead); // If the entry contains this context's stream, // we add it to the read queue. if (currentEntry.containsStream(context.id)) { context.readQueue.add(currentRead); } // If everything left is available in the resolved // queue, use it if (context.maxResolution > currentRead && context.minResolution < context.globalPointer) { return fillFromResolved(latestTokenValue, context); } // If the start is available in the resolved queue, // use it. if (context.minResolution <= context.globalPointer) { fillFromResolved(maxGlobal, context); } // Now we calculate the next entry to read. // If we have a backpointer, we'll use that for our next read. if (!runtime.backpointersDisabled && currentEntry.hasBackpointer(context.id)) { log.trace("Read_Fill_Queue[{}] Backpointer {}->{}", this, currentRead, currentEntry.getBackpointer(context.id)); currentRead = currentEntry.getBackpointer(context.id); } // Otherwise, our next read is the previous entry. else { currentRead = currentRead - 1L; } // If the next read is before or equal to the max resolved // we need to stop. if (context.maxResolution >= currentRead) { break; } } log.debug("Read_Fill_Queue[{}] Filled queue with {}", this, context.readQueue); return !context.readQueue.isEmpty(); } }