3

I've created a Flowable (RxJava v3) that parses a file. I'd like it to support backpressure. This is important because the files can be quite large, and I don't want them to be loaded into memory at once. Here is my first attempt:

public Flowable<Byte> asFlowable(InputStream is) {
    return Flowable.create(new FlowableOnSubscribe<Byte>() {

         @Override
         public void subscribe(FlowableEmitter<Byte> emitter) throws Exception {
             try (DataInputStream inputStream = new DataInputStream(is)){
                 if (inputStream.readInt() != SOME_CONSTANT) {
                     throw new IllegalArgumentException("illegal file format");
                 }

                 if (inputStream.readInt() != SOME_OTHER_CONSTANT) {
                     throw new IllegalArgumentException("illegal file format");
                 }

                 final int numItems = inputStream.readInt();

                 for(int i = 0; i < numItems; i++) {
                     if(emitter.isCancelled()) {
                         return;
                     }

                     emitter.onNext(inputStream.readByte());
                 }

                 emitter.onComplete();       
             } catch (Exception e) {
                 emitter.onError(e);
             } 
         }
     }, BackpressureStrategy.BUFFER);
}

The reason I used Flowable.create instead of Flowable.generateis because I need to validate the file, and throw errors if some magic numbers at the beginning of the file are wrong or not found. This didn't fit well with the Flowable.generate lambdas (but if you know of a better way please post it).

Ok let's assume the cold Flowable supported backpressure. Now I'd like to process it in a console-like application.

Question: I want to request a new Byte from the Flowable and print it to console each time the user presses space (similar to what more or less do in Linux). What would the best way of doing it? I intend to observe the flowable directly in the public static void main method, since I need to read and write using the console.

I've been reading the Backpressure section in RxJAva's Wiki and found this snippet:

someObservable.subscribe(new Subscriber<t>() {
    @Override
    public void onStart() {
      request(1);
    }

    @Override
    public void onCompleted() {
      // gracefully handle sequence-complete
    }

    @Override
    public void onError(Throwable e) {
      // gracefully handle error
    }

    @Override
    public void onNext(t n) {
      // do something with the emitted item "n"
      // request another item:
      request(1);
    }
});

But this confused me even more as the request method doesn't seem to exist in RxJava 3.

Mister Smith
  • 27,417
  • 21
  • 110
  • 193
  • 1
    It's `generate`. There is an updated [wiki](https://github.com/ReactiveX/RxJava/wiki/Backpressure-(2.0)) for v2+. – akarnokd Nov 20 '19 at 21:18

1 Answers1

2

Use generate, blockingSubscribe and read a line from the console:

class State {
     DataInputStream inputStream;
     int count;
     int i;
}

BufferedReader bin = new BufferedReader(new InputStreamReader(System.in));

Flowable.generate(() -> {
    State s = new State();
    s.inputStream = new DataInputStream(is);
    try {
        if (s.inputStream.readInt() != SOME_CONSTANT) {
            throw new IllegalArgumentException("illegal file format");
        }

        if (s.inputStream.readInt() != SOME_OTHER_CONSTANT) {
            throw new IllegalArgumentException("illegal file format");
        }
        s.count = s.inputStream.readInt();
    } catch (IOException ex) {
        s.inputStream.close();
        throw ex;
    }
    return s;
}, (state, emitter) -> {
    if (state.i < s.count) {
        emitter.onNext(state.inputStream.readByte());
        s.i++;
    }
    if (state.i >= s.count) {
        emitter.onComplete();
    }
}, state -> {
    state.inputStream.close();
})
.subscribeOn(Schedulers.io())
.blockingSubscribe(b -> {
    System.out.println(b);
    bin.readLine();
}, Flowable.bufferSize());
akarnokd
  • 69,132
  • 14
  • 157
  • 192
  • But according to the docs `blockingSubscribe` will block until the upstream terminates. So all elements will be first generated and stored in memory, then returned to the consumer. Like I said files can be very large, I'd like to parse on demand. – Mister Smith Nov 21 '19 at 13:16
  • You read the wrong method docs. Read [this](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Flowable.html#blockingSubscribe-io.reactivex.functions.Consumer-int-): "Backpressure: The operator consumes the source Flowable in an bounded manner (up to bufferSize outstanding request amount for items)." in other words, generate will only generate as much items as the `bufferSize` tells here. – akarnokd Nov 21 '19 at 13:20
  • I've read the link in your comment. It says: "Note that calling this method will block the caller thread until the upstream terminates normally or with an error. Therefore, calling this method from special threads such as the Android Main Thread or the Swing Event Dispatch Thread is not recommended". – Mister Smith Nov 26 '19 at 12:52
  • You stated "I'll need to observe directly in the public static void main method". There is no other way to stay on Java's main thread than blocking. Are you developing for Android or developing a Swing application? – akarnokd Nov 26 '19 at 13:35
  • It is a Java desktop app. Yes I want to block, but I don't want to block until the upstream generates all elements. I want to block element by element. Read user input, wait for a stream element, then print. – Mister Smith Nov 26 '19 at 14:43
  • It won't block until all elements because of the `bufferSize` parameter. Set it to 1 and you get one item, one console read and then the next item is generated. Have you even tried my suggestion? If you don't want to block, use `subscribe`. If you want to control the request pattern, use [`DisposableSubscriber`](http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/subscribers/DisposableSubscriber.html). – akarnokd Nov 26 '19 at 21:52
  • Oh, so buffer size of 1 can do the trick, interesting. `DisposableSubscriber` is also a good option. – Mister Smith Nov 27 '19 at 12:51
  • Well, the combination of `generate`and `blockingSubscribe` with buffer size of 1 didn't work. All elements are generated at once. I debugged it. – Mister Smith Nov 27 '19 at 14:15
  • Works for me. Did you apply any operation between `subscribeOn` and `blockingSubscribe`? – akarnokd Nov 27 '19 at 16:05
  • Yes, you were right. I was zipping the Flowable with another identical Flowable using `zipWith`. Apparently the normal version of `zipWith` does not hone backpressure, and you have to use the 4 params version specifying a buffer size of 1. – Mister Smith Nov 28 '19 at 10:51
  • Wrong. First of all, backpressure means bounded buffers and prefetch amounts, which have a default size > 1. Second, that overload exist specifically to allow the control the buffer size and thus the prefetch from the sources. – akarnokd Nov 28 '19 at 11:43