/* * Copyright 2016 the original author or 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 ratpack.stream.internal; import io.netty.util.internal.PlatformDependent; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ratpack.func.Action; import ratpack.func.Function; import ratpack.stream.TransformablePublisher; import java.util.Deque; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; public class BufferingPublisher<T> implements TransformablePublisher<T> { private static final Logger LOGGER = LoggerFactory.getLogger(BufferingPublisher.class); private static final Object ON_COMPLETE = new Object(); private static final Object ON_ERROR = new Object(); private static final Object CANCEL = new Object(); private final Action<? super T> disposer; private final Function<? super BufferedWriteStream<T>, ? extends Subscription> function; public BufferingPublisher(Action<? super T> disposer, Publisher<T> publisher) { this(disposer, write -> { return new ConnectingSubscriber<>(publisher, write); }); } public BufferingPublisher(Action<? super T> disposer, Function<? super BufferedWriteStream<T>, ? extends Subscription> function) { this.disposer = disposer; this.function = function; } @Override public void subscribe(final Subscriber<? super T> subscriber) { new BufferingSubscription(subscriber); } private static class ConnectingSubscriber<T> implements Subscriber<T>, Subscription { private final Publisher<T> publisher; private final BufferedWriteStream<T> write; private volatile Subscription upstream; private final AtomicBoolean connected = new AtomicBoolean(); private final AtomicBoolean draining = new AtomicBoolean(); private final Queue<Object> signals = PlatformDependent.newMpscQueue(); ConnectingSubscriber(Publisher<T> publisher, BufferedWriteStream<T> write) { this.publisher = publisher; this.write = write; } @Override public void request(long n) { signals.add(n); if (connected.compareAndSet(false, true)) { publisher.subscribe(this); } else { drain(); } } @Override public void cancel() { signals.add(CANCEL); drain(); } private void drain() { if (draining.compareAndSet(false, true)) { try { Subscription upstreamRead = upstream; if (upstreamRead != null) { Object signal = signals.poll(); while (signal != null) { if (signal == CANCEL) { upstreamRead.cancel(); } else { upstreamRead.request((long) signal); } signal = signals.poll(); } } } finally { draining.set(false); } if (!signals.isEmpty() && upstream != null) { drain(); } } } @Override public void onSubscribe(Subscription s) { upstream = s; drain(); } @Override public void onNext(T t) { write.item(t); } @Override public void onError(Throwable t) { write.error(t); } @Override public void onComplete() { write.complete(); } } private class BufferingSubscription implements Subscription { private final Deque<T> buffer = new ConcurrentLinkedDeque<>(); private final AtomicLong wanted = new AtomicLong(); private final AtomicBoolean draining = new AtomicBoolean(); private volatile boolean open; private volatile Subscription upstreamSubscription; private volatile Subscriber<? super T> downstream; private volatile Throwable error; private final BufferedWriteStream<T> writeStream = new WriteStream(); BufferingSubscription(Subscriber<? super T> downstream) { this.downstream = downstream; downstream.onSubscribe(this); open = true; drain(); } private void drain() { if (draining.compareAndSet(false, true)) { try { T item = buffer.poll(); while (item != null) { if (item == ON_COMPLETE) { if (downstream != null) { downstream.onComplete(); downstream = null; } } else if (item == ON_ERROR) { if (downstream != null) { assert error != null; downstream.onError(error); downstream = null; } } else { if (downstream == null || error != null) { try { disposer.execute(item); } catch (Exception e) { LOGGER.warn("exception raised disposing of " + item + " - will be ignored", e); } } else { if (wanted.get() > 0) { downstream.onNext(item); if (wanted.decrementAndGet() == 0) { break; } } else { buffer.push(item); break; } } } item = buffer.poll(); } } finally { draining.set(false); } T peek = buffer.peek(); if (peek != null && (wanted.get() > 0 || peek == ON_COMPLETE || peek == ON_ERROR)) { drain(); } } } @Override public void request(long n) { if (downstream == null) { return; } if (n < 1) { downstream.onError(new IllegalArgumentException("3.9 While the Subscription is not cancelled, Subscription.request(long n) MUST throw a java.lang.IllegalArgumentException if the argument is <= 0.")); cancel(); } if (upstreamSubscription == null) { try { upstreamSubscription = function.apply(writeStream); } catch (Exception e) { writeStream.error(e); return; } } if (wanted.get() < Long.MAX_VALUE) { long nowWanted = wanted.addAndGet(n); if (nowWanted == Long.MAX_VALUE || nowWanted < 0) { wanted.set(Long.MAX_VALUE); upstreamSubscription.request(Long.MAX_VALUE); } else { long outstanding = n - buffer.size(); if (outstanding > 0) { upstreamSubscription.request(outstanding); } } } drain(); } @Override public void cancel() { if (downstream != null) { downstream = null; if (upstreamSubscription != null) { upstreamSubscription.cancel(); } drain(); } } class WriteStream implements BufferedWriteStream<T> { @Override public void item(T item) { buffer.add(item); if (open) { drain(); } } @SuppressWarnings("unchecked") @Override public void error(Throwable throwable) { error = throwable; buffer.add((T) ON_ERROR); if (open) { drain(); } } @SuppressWarnings("unchecked") @Override public void complete() { buffer.add((T) ON_COMPLETE); if (open) { drain(); } } @Override public long getRequested() { return wanted.get(); } @Override public long getBuffered() { return buffer.size(); } @Override public boolean isCancelled() { return downstream == null; } } } }