
com.github.davidmoten.rx.internal.operators.OperatorBufferToFile Maven / Gradle / Ivy
package com.github.davidmoten.rx.internal.operators;
import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import com.github.davidmoten.rx.buffertofile.DataSerializer;
import com.github.davidmoten.rx.buffertofile.Options;
import com.github.davidmoten.util.Preconditions;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observable.Operator;
import rx.Producer;
import rx.Scheduler;
import rx.Scheduler.Worker;
import rx.Subscriber;
import rx.exceptions.Exceptions;
import rx.functions.Action0;
import rx.functions.Func0;
import rx.internal.operators.BackpressureUtils;
import rx.observers.Subscribers;
public final class OperatorBufferToFile implements Operator {
private final DataSerializer dataSerializer;
private final Scheduler scheduler;
private final Options options;
public OperatorBufferToFile(DataSerializer dataSerializer, Scheduler scheduler,
Options options) {
Preconditions.checkNotNull(dataSerializer);
Preconditions.checkNotNull(scheduler);
Preconditions.checkNotNull(options);
this.scheduler = scheduler;
this.dataSerializer = dataSerializer;
this.options = options;
}
@Override
public Subscriber super T> call(Subscriber super T> child) {
// create the file based queue
final QueueWithSubscription queue = createFileBasedQueue(dataSerializer, options);
// hold a reference to the queueProducer which will be set on
// subscription to `source`
final AtomicReference> queueProducer = new AtomicReference>();
// emissions will propagate to downstream via this worker
final Worker worker = scheduler.createWorker();
// set up the observable to read from the file based queue
Observable source = Observable
.create(new OnSubscribeFromQueue(queueProducer, queue, worker, options));
// create the parent subscriber
Subscriber parentSubscriber = new ParentSubscriber(queueProducer);
// link unsubscription
child.add(parentSubscriber);
// close and delete file based queues in RollingQueue on unsubscription
child.add(queue);
// ensure onStart not called twice
Subscriber wrappedChild = Subscribers.wrap(child);
// ensure worker gets unsubscribed (last)
child.add(worker);
// subscribe to queue
source.unsafeSubscribe(wrappedChild);
return parentSubscriber;
}
private static final boolean MEMORY_MAPPED = "true"
.equals(System.getProperty("memory.mappped"));
private static QueueWithSubscription createFileBasedQueue(
final DataSerializer dataSerializer, final Options options) {
if (MEMORY_MAPPED) {
// warning: still in development!
final int size;
if (options.rolloverSizeBytes() > Integer.MAX_VALUE) {
size = 20 * 1024 * 1024;// 20MB
} else {
size = (int) options.rolloverSizeBytes();
}
return new FileBasedSPSCQueueMemoryMapped(options.fileFactory(), size,
dataSerializer);
}
if (options.rolloverEvery() == Long.MAX_VALUE
&& options.rolloverSizeBytes() == Long.MAX_VALUE) {
// skip the Rollover version
return new QueueWithResourcesNonBlockingUnsubscribe(new FileBasedSPSCQueue(
options.bufferSizeBytes(), options.fileFactory().call(), dataSerializer));
} else {
final Func0> queueFactory = new Func0>() {
@Override
public QueueWithResources call() {
// create the file to be used for queue storage (and whose
// file name will determine the names of other files used
// for storage if multiple are required per queue)
File file = options.fileFactory().call();
return new FileBasedSPSCQueue(options.bufferSizeBytes(), file,
dataSerializer);
}
};
// the wrapping class ensures that unsubscribe happens in the same
// thread as the offer or poll which avoids the unsubscribe action
// not getting a time-slice so that the open file limit is not
// exceeded (new files are opened in the offer() call).
return new QueueWithResourcesNonBlockingUnsubscribe(new RollingSPSCQueue(
queueFactory, options.rolloverSizeBytes(), options.rolloverEvery()));
}
}
private static final class OnSubscribeFromQueue implements OnSubscribe {
private final AtomicReference> queueProducer;
private final QueueWithSubscription queue;
private final Worker worker;
private final Options options;
OnSubscribeFromQueue(AtomicReference> queueProducer,
QueueWithSubscription queue, Worker worker, Options options) {
this.queueProducer = queueProducer;
this.queue = queue;
this.worker = worker;
this.options = options;
}
@Override
public void call(Subscriber super T> child) {
QueueProducer qp = new QueueProducer(queue, child, worker, options.delayError());
queueProducer.set(qp);
child.setProducer(qp);
}
}
private static final class ParentSubscriber extends Subscriber {
private final AtomicReference> queueProducer;
ParentSubscriber(AtomicReference> queueProducer) {
this.queueProducer = queueProducer;
}
@Override
public void onStart() {
request(Long.MAX_VALUE);
}
@Override
public void onCompleted() {
queueProducer.get().onCompleted();
}
@Override
public void onError(Throwable e) {
queueProducer.get().onError(e);
}
@Override
public void onNext(T t) {
queueProducer.get().onNext(t);
}
}
private static final class QueueProducer extends AtomicLong implements Producer, Action0 {
// inherits from AtomicLong to represent the oustanding requests count
private static final long serialVersionUID = 2521533710633950102L;
private final QueueWithSubscription queue;
private final AtomicInteger drainRequested = new AtomicInteger(0);
private final Subscriber super T> child;
private final Worker worker;
private final boolean delayError;
private volatile boolean done;
// Is set just before the volatile `done` is set and read just after
// `done` is read. Thus doesn't need to be volatile.
private Throwable error = null;
QueueProducer(QueueWithSubscription queue, Subscriber super T> child, Worker worker,
boolean delayError) {
super();
this.queue = queue;
this.child = child;
this.worker = worker;
this.delayError = delayError;
this.done = false;
}
void onNext(T t) {
try {
if (!queue.offer(t)) {
onError(new RuntimeException(
"could not place item on queue (queue.offer(item) returned false), item= "
+ t));
return;
} else {
drain();
}
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
onError(e);
}
}
void onError(Throwable e) {
// must assign error before assign done = true to avoid race
// condition in finished() and also so appropriate memory barrier in
// place given error is non-volatile
error = e;
done = true;
drain();
}
void onCompleted() {
done = true;
drain();
}
@Override
public void request(long n) {
if (n > 0) {
BackpressureUtils.getAndAddRequest(this, n);
drain();
}
}
private void drain() {
// only schedule a drain if current drain has finished
// otherwise the drainRequested counter will be incremented
// and the drain loop will ensure that another drain cycle occurs if
// required
if (!child.isUnsubscribed() && drainRequested.getAndIncrement() == 0) {
worker.schedule(this);
}
}
// this method executed from drain() only
@Override
public void call() {
// catch exceptions related to file based queue in drainNow()
try {
drainNow();
} catch (Throwable e) {
child.onError(e);
}
}
private void drainNow() {
if (child.isUnsubscribed()) {
// leave drainRequested > 0 to prevent more
// scheduling of drains
return;
}
// get the number of unsatisfied requests
long requests = get();
for (;;) {
// reset drainRequested counter
drainRequested.set(1);
long emitted = 0;
while (emitted < requests) {
if (child.isUnsubscribed()) {
// leave drainRequested > 0 to prevent more
// scheduling of drains
return;
}
T item = queue.poll();
if (item == null) {
// queue is empty
if (finished()) {
return;
} else {
// another drain was requested so go
// round again but break out of this
// while loop to the outer loop so we
// can update requests and reset drainRequested
break;
}
} else {
// there was an item on the queue
if (NullSentinel.isNullSentinel(item)) {
child.onNext(null);
} else {
child.onNext(item);
}
emitted++;
}
}
// update requests with emitted value and any new requests
requests = BackpressureUtils.produced(this, emitted);
if (child.isUnsubscribed() || (requests == 0L && finished())) {
return;
}
}
}
private boolean finished() {
//cannot pass queueKnownToBeEmpty flag to this method because
//to avoid a race condition we must do an actual check on queue.isEmpty()
//after finding done is true
if (done) {
Throwable t = error;
if (queue.isEmpty()) {
// first close the queue (which in this case though
// empty also disposes of its resources)
queue.unsubscribe();
if (t != null) {
child.onError(t);
} else {
child.onCompleted();
}
// leave drainRequested > 0 so that further drain
// requests are ignored
return true;
} else if (t != null && !delayError) {
// queue is not empty but we are going to shortcut
// that because delayError is false
// first close the queue (which in this case also
// disposes of its resources)
queue.unsubscribe();
// now report the error
child.onError(t);
// leave drainRequested > 0 so that further drain
// requests are ignored
return true;
} else {
// otherwise we need to wait for all items waiting
// on the queue to be requested and delivered
// (delayError=true)
return drainRequested.compareAndSet(1, 0);
}
} else {
return drainRequested.compareAndSet(1, 0);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy