All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.apple.foundationdb.record.cursors.FlatMapPipelinedCursor Maven / Gradle / Ivy

There is a newer version: 2.8.110.0
Show newest version
/*
 * FlatMapPipelinedCursor.java
 *
 * This source file is part of the FoundationDB open source project
 *
 * Copyright 2015-2018 Apple Inc. and the FoundationDB project authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.apple.foundationdb.record.cursors;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.async.AsyncUtil;
import com.apple.foundationdb.record.ByteArrayContinuation;
import com.apple.foundationdb.record.RecordCursor;
import com.apple.foundationdb.record.RecordCursorContinuation;
import com.apple.foundationdb.record.RecordCursorProto;
import com.apple.foundationdb.record.RecordCursorResult;
import com.apple.foundationdb.record.RecordCursorStartContinuation;
import com.apple.foundationdb.record.RecordCursorVisitor;
import com.google.protobuf.ByteString;
import com.apple.foundationdb.record.SpotBugsSuppressWarnings;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * A cursor that maps elements to another cursor which is then flattened.
 *
 * 

* The cursor is pipelined, that is, it maintains up to a specified number of open cursors ahead of what it has returned, * so that work is done in parallel. *

* @param the type of elements of the source cursor * @param the type of elements of the cursor produced by the function */ @API(API.Status.MAINTAINED) public class FlatMapPipelinedCursor implements RecordCursor { @Nonnull private final RecordCursor outerCursor; @Nonnull private final BiFunction> innerCursorFunction; @Nullable private final Function checkValueFunction; @Nonnull private RecordCursorContinuation outerContinuation; @Nullable private final byte[] initialCheckValue; @Nullable private byte[] initialInnerContinuation; private final int pipelineSize; @Nonnull private final Queue pipeline; @Nullable private CompletableFuture nextFuture; @Nullable private CompletableFuture> outerNextFuture; private boolean outerExhausted = false; @Nullable private RecordCursorResult lastResult; // for detecting incorrect cursor usage private boolean mayGetContinuation = false; @SpotBugsSuppressWarnings("EI_EXPOSE_REP2") public FlatMapPipelinedCursor(@Nonnull RecordCursor outerCursor, @Nonnull BiFunction> innerCursorFunction, @Nullable Function checkValueFunction, @Nullable byte[] outerContinuation, @Nullable byte[] initialCheckValue, @Nullable byte[] initialInnerContinuation, int pipelineSize) { this.outerCursor = outerCursor; this.innerCursorFunction = innerCursorFunction; this.checkValueFunction = checkValueFunction; if (outerContinuation == null) { // Because of the semantics of byte array continuations, ByteArrayContinuation.fromNullable(null) is the // end continuation, not the start continuation! This is a bit ugly, but it's temporary until we replace // byte array continuations entirely. this.outerContinuation = RecordCursorStartContinuation.START; } else { this.outerContinuation = ByteArrayContinuation.fromNullable(outerContinuation); } this.initialInnerContinuation = initialInnerContinuation; this.initialCheckValue = initialCheckValue; this.pipelineSize = pipelineSize; this.pipeline = new ArrayDeque<>(pipelineSize); } @Nonnull @Override public CompletableFuture> onNext() { if (lastResult != null && !lastResult.hasNext()) { return CompletableFuture.completedFuture(lastResult); } mayGetContinuation = false; return AsyncUtil.whileTrue(this::tryToFillPipeline, getExecutor()).thenApply(vignore -> { lastResult = pipeline.peek().nextResult(); mayGetContinuation = !lastResult.hasNext(); return lastResult; }); } @Nonnull @Override @Deprecated public CompletableFuture onHasNext() { if (nextFuture == null) { mayGetContinuation = false; nextFuture = onNext().thenApply(RecordCursorResult::hasNext); } return nextFuture; } @Nullable @Override @Deprecated public V next() { if (!hasNext()) { throw new NoSuchElementException(); } nextFuture = null; mayGetContinuation = true; return lastResult.get(); } @Nullable @Override @Deprecated public byte[] getContinuation() { IllegalContinuationAccessChecker.check(mayGetContinuation); return lastResult.getContinuation().toBytes(); } @Override public void close() { if (nextFuture != null) { nextFuture.cancel(false); nextFuture = null; } while (!pipeline.isEmpty()) { pipeline.remove().close(); } if (outerNextFuture != null) { outerNextFuture.cancel(false); outerNextFuture = null; } outerCursor.close(); } @Nonnull @Override @Deprecated public NoNextReason getNoNextReason() { return lastResult.getNoNextReason(); } @Override public boolean accept(@Nonnull RecordCursorVisitor visitor) { if (visitor.visitEnter(this)) { outerCursor.accept(visitor); } return visitor.visitLeave(this); } @Nonnull @Override public Executor getExecutor() { return outerCursor.getExecutor(); } /** * Take items from inner cursor and put in pipeline until no more or a mapped cursor item is available. * @return a future that will complete with {@code false} if an item is available or none will ever be, or with {@code true} if this method should be called to try again */ protected CompletableFuture tryToFillPipeline() { // Clear pipeline entries left behind by exhausted inner cursors. while (!pipeline.isEmpty() && pipeline.peek().doesNotHaveReturnableResult()) { pipeline.remove().close(); } while (!outerExhausted && pipeline.size() < pipelineSize) { if (outerNextFuture == null) { outerNextFuture = outerCursor.onNext(); } if (!outerNextFuture.isDone()) { // Still waiting for outer future. Check back when it has finished. final PipelineQueueEntry nextEntry = pipeline.peek(); if (nextEntry == null) { return outerNextFuture.thenApply(vignore -> true); // loop back to process outer result } else { // keep looping unless we get something from the next entry's inner cursor or the next cursor is ready final CompletableFuture innerPipelineFuture = nextEntry.getNextInnerPipelineFuture(); return CompletableFuture.anyOf(outerNextFuture, innerPipelineFuture).thenApply(vignore -> !innerPipelineFuture.isDone() || innerPipelineFuture.join().doesNotHaveReturnableResult()); } } final RecordCursorResult outerResult = outerNextFuture.join(); if (outerResult.hasNext()) { final RecordCursorContinuation priorOuterContinuation = outerContinuation; final T outerValue = outerResult.get(); final byte[] outerCheckValue = checkValueFunction == null ? null : checkValueFunction.apply(outerValue); byte[] innerContinuation = null; if (initialInnerContinuation != null) { // Check if the outer cursor is positioned to the same place as before, by comparing the outer // check value to the initial check value used to build the cursor. If they match (or one is missing), // use the given initial inner continuation. Otherwise, something about the outer cursor changed, // so we should start the inner cursor from the beginning. if (initialCheckValue == null || outerCheckValue == null || Arrays.equals(initialCheckValue, outerCheckValue)) { innerContinuation = initialInnerContinuation; initialInnerContinuation = null; } } final RecordCursor innerCursor = innerCursorFunction.apply(outerValue, innerContinuation); outerContinuation = outerResult.getContinuation(); pipeline.add(new PipelineQueueEntry(innerCursor, priorOuterContinuation, outerResult, outerCheckValue)); outerNextFuture = null; // done with this future, advance outer cursor next time // keep looping to fill pipeline } else { // don't have next, and won't ever with this cursor // Add sentinel to end of pipeline pipeline.add(new PipelineQueueEntry(null, outerContinuation, outerResult, null)); outerExhausted = true; // Wait for next entry, as if pipeline were full break; } } // One of the following holds: // 1) The pipeline is full. // 2) We just added something to it. // 3) The outer cursor is exhausted and so the last element in the pipeline is a sentinel that will never be removed. // In any case, it contains an entry so pipeline.peek() will be non-null. return pipeline.peek().getNextInnerPipelineFuture().thenApply(PipelineQueueEntry::doesNotHaveReturnableResult); } private class PipelineQueueEntry { final RecordCursor innerCursor; final RecordCursorContinuation priorOuterContinuation; final RecordCursorResult outerResult; final byte[] outerCheckValue; private CompletableFuture> innerFuture; public PipelineQueueEntry(RecordCursor innerCursor, RecordCursorContinuation priorOuterContinuation, RecordCursorResult outerResult, byte[] outerCheckValue) { this.innerCursor = innerCursor; this.priorOuterContinuation = priorOuterContinuation; this.outerResult = outerResult; this.outerCheckValue = outerCheckValue; } @Nonnull public CompletableFuture getNextInnerPipelineFuture() { if (innerFuture == null) { if (innerCursor == null) { innerFuture = CompletableFuture.completedFuture(RecordCursorResult.exhausted()); } else { innerFuture = innerCursor.onNext(); } } return innerFuture.thenApply(vignore -> this); } public boolean doesNotHaveReturnableResult() { if (innerCursor == null || // Hit sentinel, so we have a returnable result innerFuture == null || // Inner future hasn't been started yet. !innerFuture.isDone()) { // No result yet. Don't know whether result will be returnable. return false; } final RecordCursorResult innerResult = innerFuture.join(); if (innerResult.hasNext()) { return false; // a result with a value is returnable by the cursor } else { // inner cursor exhausted // If the inner cursor is exhausted, we should return the first value from the next inner cursor. // If the inner cursor stopped for any other reason, it's not valid to take from later in the pipeline. return innerResult.getNoNextReason().isSourceExhausted(); } } public void close() { if (innerFuture != null && innerFuture.cancel(false)) { innerCursor.close(); } } @Nonnull public RecordCursorResult nextResult() { // Only called after the future from getNextInnerPipelineFuture() has completed, so this join() is non-blocking. final RecordCursorResult innerResult = innerFuture.join(); final RecordCursorResult result; if (innerResult.hasNext()) { result = RecordCursorResult.withNextValue(innerResult.get(), toContinuation()); } else { NoNextReason reason; if (innerResult.getNoNextReason().isSourceExhausted()) { // If the outer cursor had another result, we would have skipped over this exhausted result from // the inner cursor and moved on to the next inner cursor (as indicated by // doesNotHaveReturnableResult()). Thus, the outer cursor must be stopped. reason = outerResult.getNoNextReason(); } else { reason = innerResult.getNoNextReason(); } result = RecordCursorResult.withoutNextValue(toContinuation(), reason); } innerFuture = null; return result; } @Nonnull private Continuation toContinuation() { return new Continuation<>(priorOuterContinuation, outerResult, outerCheckValue, innerFuture.join()); } } private static class Continuation implements RecordCursorContinuation { @Nonnull private final RecordCursorContinuation priorOuterContinuation; @Nonnull private final RecordCursorResult outerResult; @Nullable private final byte[] outerCheckValue; @Nonnull private final RecordCursorResult innerResult; public Continuation(@Nonnull RecordCursorContinuation priorOuterContinuation, @Nonnull RecordCursorResult outerResult, @Nullable byte[] outerCheckValue, @Nonnull RecordCursorResult innerResult) { this.priorOuterContinuation = priorOuterContinuation; this.outerResult = outerResult; this.outerCheckValue = outerCheckValue; this.innerResult = innerResult; } @Override public boolean isEnd() { return outerResult.getContinuation().isEnd() && innerResult.getContinuation().isEnd(); } @Nullable @Override public byte[] toBytes() { if (isEnd()) { return null; } final RecordCursorProto.FlatMapContinuation.Builder builder = RecordCursorProto.FlatMapContinuation.newBuilder(); final RecordCursorContinuation innerContinuation = innerResult.getContinuation(); if (innerContinuation.isEnd()) { // This was the last of the inner cursor. Take continuation from outer after it. builder.setOuterContinuation(ByteString.copyFrom(outerResult.getContinuation().toBytes())); } else { // This was in the middle of the inner cursor. Take continuation from outer before it and arrange to skip to it. final byte[] priorOuterContinuationBytes = priorOuterContinuation.toBytes(); if (priorOuterContinuationBytes != null) { // isn't start or end continuation builder.setOuterContinuation(ByteString.copyFrom(priorOuterContinuation.toBytes())); } if (outerCheckValue != null) { builder.setCheckValue(ByteString.copyFrom(outerCheckValue)); } builder.setInnerContinuation(ByteString.copyFrom(innerContinuation.toBytes())); } return builder.build().toByteArray(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy