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

ratpack.stream.internal.BufferingPublisher Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc-1
Show newest version
/*
 * 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 implements TransformablePublisher {

  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 disposer;
  private final Function, Subscription> function;

  public BufferingPublisher(Action disposer, Publisher publisher) {
    this(disposer, write -> {
      return new ConnectingSubscriber<>(publisher, write);
    });
  }

  public BufferingPublisher(Action disposer, Function, Subscription> function) {
    this.disposer = disposer;
    this.function = function;
  }

  @Override
  public void subscribe(final Subscriber subscriber) {
    new BufferingSubscription(subscriber);
  }

  private static class ConnectingSubscriber implements Subscriber, Subscription {

    private final Publisher publisher;
    private final BufferedWriteStream write;

    private volatile Subscription upstream;

    private final AtomicBoolean connected = new AtomicBoolean();
    private final AtomicBoolean draining = new AtomicBoolean();
    private final Queue signals = PlatformDependent.newMpscQueue();

    public ConnectingSubscriber(Publisher publisher, BufferedWriteStream 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 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 downstream;
    private volatile Throwable error;

    private final BufferedWriteStream writeStream = new WriteStream();

    public BufferingSubscription(Subscriber 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 = nowWanted - buffer.size();
          if (outstanding > 0) {
            upstreamSubscription.request(outstanding);
          }
        }
      }
      drain();
    }

    @Override
    public void cancel() {
      downstream = null;
      if (upstreamSubscription != null) {
        upstreamSubscription.cancel();
      }
      drain();
    }

    class WriteStream implements BufferedWriteStream {
      @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();
      }
    }

  }
}