/*
* Copyright 2008-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 com.nominanuda.zen.reactivestreams;
import static com.nominanuda.zen.common.Check.notNull;
import static com.nominanuda.zen.reactivestreams.ReactiveUtils.cappedSum;
import static java.lang.Math.max;
import static java.lang.Math.min;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.reactivestreams.Processor;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.RingBuffer;
//TODO only one in one out functions supported
public class BufferingProcessor<T,R> extends TransformingProcessor<T, R> {
public static final int DEFAULT_RING_BUFFER_SIZE = 1024;
private Subscriber<? super R> subscriber;
private RingBuffer<ReactiveEvent<R>> ringBuffer;
private boolean syncMode = true;
private Executor workerPool;
private int bufferSize = DEFAULT_RING_BUFFER_SIZE;
@Override
public final void onSubscribe(Subscription s) {
if(ringBuffer == null) {
ringBuffer = RingBuffer.createSingleProducer(new EventFactory<ReactiveEvent<R>>() {
@Override
public ReactiveEvent<R> newInstance() {
return new ReactiveEvent<R>();
}
}, bufferSize);
}
this.upstreamSubscription = s;
}
@Override
public final void subscribe(Subscriber<? super R> s) {
if(s == null) {
throw new NullPointerException("rule 1.9");
} else if(subscriber != null) {
s.onSubscribe(new SubscriptionImpl());
s.onError(new IllegalStateException("this publisher does not allow multiple subscribers"));
} else {
subscriber = s;
subscriber.onSubscribe(downstreamSubscription);
}
}
private volatile long last = -1;
private volatile long maxPublished = -1;
public final void onNextOrComplete(final @Nullable /*null means complete*/ T t) {
final long claimed = ringBuffer.next();
//TODO avoid new Runnable()
if(syncMode) {
try {
if(t == null) {
ringBuffer.get(claimed).complete();
} else {
//potentially blocking
R r = getFunction().apply(t);
//
ringBuffer.get(claimed).set(r);
}
ringBuffer.publish(claimed);
if(claimed > maxPublished) {//TODO race cond
maxPublished = claimed;
}
drain();
} catch(Throwable e) {//we risk not to publish a claimed seq but in this case the whole RingBuffer is invalid and not used anymore
onError(e);
}
} else {
workerPool.execute(() -> {
try {
if(t == null) {
ringBuffer.get(claimed).complete();
} else {
//potentially blocking
R r = getFunction().apply(t);
//
ringBuffer.get(claimed).set(r);
}
ringBuffer.publish(claimed);
if(claimed > maxPublished) {//TODO race cond
maxPublished = claimed;
}
drain();
} catch(Throwable e) {//we risk not to publish a claimed seq but in this case the whole RingBuffer is invalid and not used anymore
onError(e);
}
});
}
}
private final AtomicBoolean drainGuard = new AtomicBoolean(true);
private void drain() {
if(drainGuard.compareAndSet(true, false)) {
for(long z = last + 1; z <= maxPublished/*cur*/; z++) {
ReactiveEvent<R> ar = ringBuffer.get(z);
if(ar.isSet()) {
if(! ar.isComplete()) {
downDem--;
}
ar.flushTo(subscriber);
last = z;
} else {
break;
}
}
updateUpstreamSubscription();
drainGuard.set(true);
}
}
/**
* @see {@link Processor#onNext(Object)}
*/
@Override
public final void onNext(final T t) {
upDem--;
onNextOrComplete(notNull(t));
}
/**
* @see {@link Processor#onComplete()}
*/
@Override
public final void onComplete() {
onNextOrComplete(null);
}
boolean upstreamCompleted = false;
@Override
protected void out(R r) {
throw new UnsupportedOperationException();
}
/**
* @see {@link Processor#onError(Throwable)}
*/
@Override
public final void onError(Throwable t) {
subscriber.onError(t);
//TODO
}
///// DEMAND MATHS AND BUFFER FLUSHING
private volatile long downDem = 0;
private volatile long upDem = 0;
private boolean downUnbounded = false;
private final Consumer<Long> onDownstreamSubReq = (n) -> {
if(! downUnbounded) {
downDem = cappedSum(downDem, n);
if(downDem == Long.MAX_VALUE) {
downUnbounded = true;
}
}
updateUpstreamSubscription();
};
private Subscription upstreamSubscription;
private DelegatingSubscription downstreamSubscription = new DelegatingSubscription();
{
downstreamSubscription.onRequest(onDownstreamSubReq);
}
//!!!!! at all times
//downDem <= Long.MAX_VALUE; see onDownstreamSubReq
//upDem <= downDem - busySlots(aka bufferSize - freeSlots)
//upDem <= freeSlots
//upDem <= Long.MAX_VALUE; see upstreamRequest()
private final AtomicBoolean updateUpstreamSubscriptionGuard = new AtomicBoolean(true);
private void updateUpstreamSubscription() {
if(updateUpstreamSubscriptionGuard.compareAndSet(true, false)) {
long busySlots = maxPublished - last + 1;/*room for onComplete*/
long freeSlots = bufferSize - busySlots;
long targetUpDem = downUnbounded
? freeSlots
: min(max(downDem - busySlots, 0), freeSlots);
long deltaUpDem = targetUpDem - upDem;
if(deltaUpDem > 0) {
upstreamRequest(deltaUpDem);
} else {
System.err.println("PANIC 11");
}
updateUpstreamSubscriptionGuard.set(true);
}
}
private void upstreamRequest(long n) {
if(upstreamSubscription != null) {
upstreamSubscription.request(n);
} else {
System.err.println("PANIC 22");
}
}
//configuration
/**
* the {@link Executor} or {@link ExecutorService} to run the transformation tasks
* if it allows parallel executions, reordering of results is managed by this class.
* {@link Executor#execute(Runnable)} is called in the context of {@link #onNext(Object)}
* and the default is to do so on the same {@link Thread}, so call this method if the
* applied transformation is blocking, or parallel executions are desired.
*
* @param workerPool
*/
public final void setWorkerPool(Executor workerPool) {
this.workerPool = workerPool;
this.syncMode = false;
}
/**
* the {@link RingBuffer} buffer size. Shoud be a power of 2. The default is {@value #DEFAULT_RING_BUFFER_SIZE}
* @param bufferSize
*/
public final void setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
}
}