
net.oneandone.troilus.ResultListSubscription Maven / Gradle / Ivy
/*
* Copyright 1&1 Internet AG, https://github.com/1and1/
*
* 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 net.oneandone.troilus;
import java.io.Closeable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import net.oneandone.troilus.java7.FetchingIterator;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Queues;
import com.google.common.util.concurrent.ListenableFuture;
/**
* ResultListSubscription
*
* @param the element type
*/
class ResultListSubscription implements Subscription {
private final DatabaseSource databaseSource;
private final SubscriberNotifier subscriberNotifier;
/**
* @param subscriber the subscriber
* @param iterator the underlying iterator
*/
public ResultListSubscription(Subscriber super T> subscriber, FetchingIterator iterator) {
Executor executor = Executors.newCachedThreadPool();
this.subscriberNotifier = new SubscriberNotifier<>(executor, subscriber);
this.databaseSource = new DatabaseSource<>(executor, subscriberNotifier, iterator);
subscriberNotifier.emitNotification(new OnSubscribe());
}
private class OnSubscribe extends SubscriberNotifier.Notification {
@Override
public void signalTo(Subscriber super T> subscriber) {
subscriber.onSubscribe(ResultListSubscription.this);
}
}
@Override
public void cancel() {
databaseSource.close();
}
@Override
public void request(long n) {
if(n <= 0) {
// https://github.com/reactive-streams/reactive-streams#3.9
subscriberNotifier.emitNotification(new OnError(new IllegalArgumentException("Non-negative number of elements must be requested: https://github.com/reactive-streams/reactive-streams#3.9")));
} else {
databaseSource.request(n);
}
}
private static class OnError extends SubscriberNotifier.TerminatingNotification {
private static final Logger LOG = LoggerFactory.getLogger(OnError.class);
private final Throwable error;
public OnError(Throwable error) {
this.error = error;
}
@Override
public void signalTo(Subscriber super R> subscriber) {
LOG.debug("processing error occured", error);
try {
subscriber.onError(error);
} catch (RuntimeException rt) {
LOG.warn("error occured by notifying error ", rt);
}
}
}
private static final class DatabaseSource implements Closeable {
private final Object dbQueryLock = new Object();
private final AtomicBoolean isOpen = new AtomicBoolean(true);
private final AtomicLong numRequestedReads = new AtomicLong();
private final Executor executor;
private final SubscriberNotifier notifier;
private final FetchingIterator iterator;
private Runnable runningDatabaseQuery = null;
DatabaseSource(Executor executor, SubscriberNotifier notifier, FetchingIterator iterator) {
this.executor = executor;
this.notifier = notifier;
this.iterator = iterator;
}
@Override
public void close() {
isOpen.set(false);
}
void request(long num) {
if (isOpen.get()) {
numRequestedReads.addAndGet(num);
processReadRequests();
}
}
private void processReadRequests() {
if (isOpen.get()) {
processAvailableDatabaseRecords();
// more db records required?
if (numRequestedReads.get() > 0) {
// [synchronization note] under some circumstances the method requestDatabaseForMoreRecords()
// will be executed without the need of more records. However, it does not matter
requestDatabaseForMoreRecords();
}
}
}
private void processAvailableDatabaseRecords() {
while ((iterator.getAvailableWithoutFetching() > 0) && numRequestedReads.get() > 0) {
try {
numRequestedReads.decrementAndGet();
notifier.emitNotification(new OnNext(iterator.next()));
} catch (RuntimeException rt) {
notifier.emitNotification(new OnError(rt));
}
}
}
void requestDatabaseForMoreRecords() {
// more data to fetch available?
if (iterator.isFullyFetched()) {
// no, all data has been read
notifier.emitNotification(new OnComplete());
// yes, more elements can be fetched
} else {
synchronized (dbQueryLock) {
// submit an async database query (if not already running)
if (runningDatabaseQuery == null) {
ListenableFuture future = iterator.fetchMoreResultsAsync();
Runnable databaseRequest = new Runnable() {
@Override
public void run() {
synchronized (dbQueryLock) {
runningDatabaseQuery = null;
}
processReadRequests();
}
};
future.addListener(databaseRequest, executor);
runningDatabaseQuery = databaseRequest;
}
}
}
}
private class OnNext extends SubscriberNotifier.Notification {
private final R element;
public OnNext(R element) {
this.element = element;
}
@Override
public void signalTo(Subscriber super R> subscriber) {
subscriber.onNext(element);
}
}
private class OnComplete extends SubscriberNotifier.TerminatingNotification {
@Override
public void signalTo(Subscriber super R> subscriber) {
subscriber.onComplete();
}
}
}
private static final class SubscriberNotifier implements Runnable {
private final ConcurrentLinkedQueue> notifications = Queues.newConcurrentLinkedQueue();
private final Executor executor;
private final AtomicBoolean isOpen = new AtomicBoolean(true);
private final Subscriber super R> subscriber;
public SubscriberNotifier(Executor executor, Subscriber super R> subscriber) {
this.executor = executor;
this.subscriber = subscriber;
}
private void close() {
isOpen.set(false);
notifications.clear();
}
public void emitNotification(Notification notification) {
if (isOpen.get()) {
if (notifications.offer(notification)) {
tryScheduleToExecute();
}
}
}
private final void tryScheduleToExecute() {
try {
executor.execute(this);
} catch (Throwable t) {
close(); // no further notifying (executor does not work anyway)
subscriber.onError(t);
}
}
// main "event loop"
@Override
public final void run() {
if (isOpen.get()) {
synchronized (subscriber) {
try {
Notification notification = notifications.poll();
if (notification != null) {
if (notification.isTerminating()) {
close();
}
notification.signalTo(subscriber);
}
} finally {
if(!notifications.isEmpty()) {
tryScheduleToExecute();
}
}
}
}
}
private static abstract class Notification {
abstract void signalTo(Subscriber super R> subscriber);
boolean isTerminating() {
return false;
}
};
// Once a terminal state has been signaled (onError, onComplete) it is REQUIRED that no further signals occur
private static abstract class TerminatingNotification extends Notification {
boolean isTerminating() {
return true;
}
};
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy