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

org.xbib.helianthus.common.stream.DefaultStreamMessage Maven / Gradle / Ivy

package org.xbib.helianthus.common.stream;

import static java.util.Objects.requireNonNull;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.xbib.helianthus.common.util.Exceptions;

import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.Supplier;
import java.util.logging.Logger;

/**
 * A {@link StreamMessage} which buffers the elements to be signaled into a {@link Queue}.
 *
 * 

This class implements the {@link StreamWriter} interface as well. A written element will be buffered * into the {@link Queue} until a {@link Subscriber} consumes it. Use {@link StreamWriter#onDemand(Runnable)} * to control the rate of production so that the {@link Queue} does not grow up infinitely. * *

{@code
 * void stream(QueueBasedPublished pub, int start, int end) {
 *     // Write 100 integers at most.
 *     int actualEnd = (int) Math.min(end, start + 100L);
 *     int i;
 *     for (i = start; i < actualEnd; i++) {
 *         pub.write(i);
 *     }
 *
 *     if (i == end) {
 *         // Wrote the last element.
 *         return;
 *     }
 *
 *     pub.onDemand(() -> stream(pub, i, end));
 * }
 *
 * final QueueBasedPublisher myPub = new QueueBasedPublisher<>();
 * stream(myPub, 0, Integer.MAX_VALUE);
 * }
* * @param the type of element signaled */ public class DefaultStreamMessage implements StreamMessage, StreamWriter { private static final Logger logger = Logger.getLogger(DefaultStreamMessage.class.getName()); private static final CloseEvent SUCCESSFUL_CLOSE = new CloseEvent(null); private static final CloseEvent CANCELLED_CLOSE = new CloseEvent( Exceptions.clearTrace(CancelledSubscriptionException.get())); @SuppressWarnings("rawtypes") private static final AtomicReferenceFieldUpdater subscriptionUpdater = AtomicReferenceFieldUpdater.newUpdater(DefaultStreamMessage.class, SubscriptionImpl.class, "subscription"); @SuppressWarnings("rawtypes") private static final AtomicLongFieldUpdater demandUpdater = AtomicLongFieldUpdater.newUpdater(DefaultStreamMessage.class, "demand"); @SuppressWarnings("rawtypes") private static final AtomicReferenceFieldUpdater stateUpdater = AtomicReferenceFieldUpdater.newUpdater(DefaultStreamMessage.class, State.class, "state"); private final Queue queue; private final CompletableFuture closeFuture = new CompletableFuture<>(); @SuppressWarnings("unused") private volatile SubscriptionImpl subscription; // set only via subscriptionUpdater @SuppressWarnings("unused") private volatile long demand; // set only via demandUpdater @SuppressWarnings("FieldMayBeFinal") private volatile State state = State.OPEN; private volatile boolean wroteAny; public DefaultStreamMessage() { this(new ConcurrentLinkedQueue<>()); } public DefaultStreamMessage(Queue queue) { this.queue = requireNonNull(queue, "queue"); } @Override public boolean isOpen() { return state == State.OPEN; } @Override public boolean isEmpty() { return !isOpen() && !wroteAny; } @Override public void subscribe(Subscriber subscriber) { requireNonNull(subscriber, "subscriber"); subscribe0(new SubscriptionImpl(this, subscriber, null)); } @Override public void subscribe(Subscriber subscriber, Executor executor) { requireNonNull(subscriber, "subscriber"); requireNonNull(executor, "executor"); subscribe0(new SubscriptionImpl(this, subscriber, executor)); } private void subscribe0(SubscriptionImpl subscription) { if (!subscriptionUpdater.compareAndSet(this, null, subscription)) { throw new IllegalStateException( "subscribed by other subscriber already: " + this.subscription.subscriber()); } final Executor executor = subscription.executor(); if (executor != null) { executor.execute(() -> subscription.subscriber().onSubscribe(subscription)); } else { subscription.subscriber().onSubscribe(subscription); } } @Override public void abort() { final SubscriptionImpl currentSubscription = subscription; if (currentSubscription != null) { currentSubscription.cancel(); return; } final SubscriptionImpl newSubscription = new SubscriptionImpl(this, AbortingSubscriber.INSTANCE, null); if (subscriptionUpdater.compareAndSet(this, null, newSubscription)) { newSubscription.subscriber().onSubscribe(newSubscription); } else { subscription.cancel(); } } @Override public boolean write(T obj) { requireNonNull(obj, "obj"); if (!isOpen()) { return false; } wroteAny = true; pushObject(obj); return true; } @Override public boolean write(Supplier supplier) { return write(supplier.get()); } @Override public CompletableFuture onDemand(Runnable task) { requireNonNull(task, "task"); final AwaitDemandFuture f = new AwaitDemandFuture(); if (!isOpen()) { f.completeExceptionally(ClosedPublisherException.get()); } else { pushObject(f); } return f.thenRun(task); } private void pushObject(Object obj) { queue.add(obj); notifySubscriber(); } protected void notifySubscriber() { final SubscriptionImpl subscription = this.subscription; if (subscription == null) { return; } final Queue queue = this.queue; if (queue.isEmpty()) { return; } final Executor executor = subscription.executor(); if (executor != null) { executor.execute(() -> notifySubscribers0(subscription, queue)); } else { notifySubscribers0(subscription, queue); } } private void notifySubscribers0(SubscriptionImpl subscription, Queue queue) { if (state == State.CLEANUP) { cleanup(); return; } final Subscriber subscriber = subscription.subscriber(); for (;;) { final Object o = queue.peek(); if (o == null) { break; } if (o instanceof CloseEvent) { notifySubscriberWithCloseEvent(subscriber, (CloseEvent) o); break; } if (o instanceof AwaitDemandFuture) { if (notifyCompletableFuture(queue)) { // Notified successfully. continue; } else { // Not enough demand. break; } } if (!notifySubscriber(subscriber, queue)) { // Not enough demand. break; } } } private boolean notifySubscriber(Subscriber subscriber, Queue queue) { for (;;) { final long demand = this.demand; if (demand == 0) { break; } if (demand == Long.MAX_VALUE || demandUpdater.compareAndSet(this, demand, demand - 1)) { @SuppressWarnings("unchecked") final T o = (T) queue.remove(); onRemoval(o); subscriber.onNext(o); return true; } } return false; } private boolean notifyCompletableFuture(Queue queue) { if (demand == 0) { return false; } @SuppressWarnings("unchecked") final CompletableFuture f = (CompletableFuture) queue.remove(); f.complete(null); return true; } private void notifySubscriberWithCloseEvent(Subscriber subscriber, CloseEvent o) { setState(State.CLEANUP); try { final Throwable cause = o.cause(); if (cause == null) { try { subscriber.onComplete(); } finally { closeFuture.complete(null); } } else { try { if (!o.isCancelled()) { subscriber.onError(cause); } } finally { closeFuture.completeExceptionally(cause); } } } finally { cleanup(); } } protected void onRemoval(T obj) { } @Override public CompletableFuture closeFuture() { return closeFuture; } @Override public void close() { if (setState(State.CLOSED)) { pushObject(SUCCESSFUL_CLOSE); } } @Override public void close(Throwable cause) { requireNonNull(cause, "cause"); if (cause instanceof CancelledSubscriptionException) { throw new IllegalArgumentException("cause: " + cause + " (must use Subscription.cancel())"); } if (setState(State.CLOSED)) { pushObject(new CloseEvent(cause)); } } private boolean setState(State state) { if (state == State.OPEN) { throw new IllegalStateException(); } return stateUpdater.compareAndSet(this, State.OPEN, state); } private void cleanup() { final Throwable cause = ClosedPublisherException.get(); for (;;) { final Object e = queue.poll(); if (e == null) { break; } if (e instanceof CloseEvent) { final Throwable closeCause = ((CloseEvent) e).cause(); if (closeCause != null) { closeFuture.completeExceptionally(closeCause); } else { closeFuture.complete(null); } continue; } if (e instanceof CompletableFuture) { ((CompletableFuture) e).completeExceptionally(cause); } @SuppressWarnings("unchecked") T obj = (T) e; onRemoval(obj); } } private enum State { /** * The initial state. Will enter {@link #CLOSED} or {@link #CLEANUP}. */ OPEN, /** * {@link #close()} or {@link #close(Throwable)} has been called. Will enter {@link #CLEANUP} after * {@link Subscriber#onComplete()} or {@link Subscriber#onError(Throwable)} is invoked. */ CLOSED, /** * Anything in the queue must be cleaned up. * Enters this state when there's no chance of consumption by subscriber. * i.e. when any of the following methods are invoked: *
    *
  • {@link Subscription#cancel()}
  • *
  • {@link #abort()} (via {@link AbortingSubscriber})
  • *
  • {@link Subscriber#onComplete()}
  • *
  • {@link Subscriber#onError(Throwable)}
  • *
*/ CLEANUP } private static final class SubscriptionImpl implements Subscription { private final DefaultStreamMessage publisher; private final Subscriber subscriber; private final Executor executor; @SuppressWarnings("unchecked") SubscriptionImpl(DefaultStreamMessage publisher, Subscriber subscriber, Executor executor) { this.publisher = publisher; this.subscriber = (Subscriber) subscriber; this.executor = executor; } Subscriber subscriber() { return subscriber; } Executor executor() { return executor; } @Override public void request(long n) { if (n <= 0) { throw new IllegalArgumentException("n: " + n + " (expected: > 0)"); } for (;;) { final long oldDemand = publisher.demand; final long newDemand; if (oldDemand >= Long.MAX_VALUE - n) { newDemand = Long.MAX_VALUE; } else { newDemand = oldDemand + n; } if (demandUpdater.compareAndSet(publisher, oldDemand, newDemand)) { if (oldDemand == 0) { publisher.notifySubscriber(); } break; } } } @Override public void cancel() { if (publisher.setState(State.CLEANUP)) { final CloseEvent closeEvent = Exceptions.isVerbose() ? new CloseEvent(CancelledSubscriptionException.get()) : CANCELLED_CLOSE; publisher.pushObject(closeEvent); } } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Subscription:publisher=") .append(publisher) .append(",demand=") .append(publisher.demand) .append(",executor=") .append(executor); return sb.toString(); } } private static final class AwaitDemandFuture extends CompletableFuture { } private static final class CloseEvent { private final Throwable cause; CloseEvent(Throwable cause) { this.cause = cause; } boolean isCancelled() { return cause instanceof CancelledSubscriptionException; } Throwable cause() { return cause; } @Override public String toString() { if (cause == null) { return "CloseEvent"; } else { return "CloseEvent(" + cause + ')'; } } } }