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

net.oneandone.troilus.ResultListSubscription Maven / Gradle / Ivy

There is a newer version: 0.18
Show newest version
/*
 * 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 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 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 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 subscriber) {
                subscriber.onNext(element);
            }
        }


        private class OnComplete extends SubscriberNotifier.TerminatingNotification {
            
            @Override
            public void signalTo(Subscriber 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 subscriber;
        
        public SubscriberNotifier(Executor executor, Subscriber 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 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