/*
* Licensed to Crate under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership. Crate licenses this file
* to you 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial
* agreement.
*/
package io.crate.testing;
import io.crate.concurrent.CompletableFutures;
import io.crate.data.BatchIterator;
import io.crate.data.Columns;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.PrimitiveIterator;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* An BatchIterator implementation which delegates to another one, but adds "fake" batches.
*/
public class BatchSimulatingIterator implements BatchIterator {
private final BatchIterator delegate;
private final int batchSize;
private final Executor executor;
private final PrimitiveIterator.OfLong loadNextDelays;
private final int numBatches;
private int numSuccessMoveNextCallsInBatch = 0;
private int currentBatch = 0;
private final AtomicBoolean currentlyLoading = new AtomicBoolean(false);
/**
* @param batchSize how many {@link #moveNext()} calls are allowed per batch before it returns false
* @param maxAdditionalFakeBatches how many {@link #loadNextBatch()} calls are allowed after {@code delegate.allLoaded()} is true.
* (This is an upper limit, if a consumer calls moveNext correctly, the actual number may be lower)
*/
public BatchSimulatingIterator(BatchIterator delegate,
int batchSize,
int maxAdditionalFakeBatches,
@Nullable Executor executor) {
assert batchSize > 0 : "batchSize must be greater than 0. It is " + batchSize;
assert maxAdditionalFakeBatches > 0
: "maxAdditionalFakeBatches must be greater than 0. It is " + maxAdditionalFakeBatches;
this.delegate = delegate;
this.numBatches = maxAdditionalFakeBatches;
this.batchSize = batchSize;
this.loadNextDelays = new Random(System.currentTimeMillis()).longs(0, 100).iterator();
this.executor = executor == null ? ForkJoinPool.commonPool() : executor;
}
@Override
public Columns rowData() {
return delegate.rowData();
}
@Override
public void moveToStart() {
ensureNotLoading();
currentBatch = 0;
delegate.moveToStart();
numSuccessMoveNextCallsInBatch = 0;
}
@Override
public boolean moveNext() {
ensureNotLoading();
if (numSuccessMoveNextCallsInBatch == batchSize) {
return false;
}
if (delegate.moveNext()) {
numSuccessMoveNextCallsInBatch++;
return true;
}
if (delegate.allLoaded()) {
currentBatch = numBatches;
}
return false;
}
private void ensureNotLoading() {
if (currentlyLoading.get()) {
throw new IllegalStateException("Call not allowed during load operation");
}
}
@Override
public void close() {
delegate.close();
}
@Override
public CompletionStage<?> loadNextBatch() {
if (!currentlyLoading.compareAndSet(false, true)) {
return CompletableFutures.failedFuture(new IllegalStateException("loadNextBatch call during load operation"));
}
if (delegate.allLoaded()) {
currentBatch++;
if (currentBatch > numBatches) {
currentlyLoading.compareAndSet(true, false);
return CompletableFutures.failedFuture(new IllegalStateException("Iterator already fully loaded"));
}
return CompletableFuture.runAsync(() -> {
try {
Thread.sleep(loadNextDelays.nextLong());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
numSuccessMoveNextCallsInBatch = 0;
}, executor).thenAccept(i -> currentlyLoading.compareAndSet(true, false));
} else {
return delegate.loadNextBatch().thenAccept(i -> currentlyLoading.compareAndSet(true, false));
}
}
@Override
public boolean allLoaded() {
return delegate.allLoaded() && currentBatch >= numBatches;
}
@Override
public void kill(@Nonnull Throwable throwable) {
delegate.kill(throwable);
}
}