4

I know this breaks a lot of Rx rules, but I really like RxJava-JDBC and so do my teammates. Relational databases are very core to what we do and so is Rx.

However there are some occasions where we do not want to emit as an Observable<ResultSet> but would rather just have a pull-based Java 8 Stream<ResultSet> or Kotlin Sequence<ResultSet>. But we are very accustomed to the RxJava-JDBC library which only returns an Observable<ResultSet>.

Therefore, I am wondering if there is a way I can turn an Observable<ResultSet> into a Sequence<ResultSet> using an extension function, and not do any intermediary collection or toBlocking() calls. Below is all I have so far but my head is spinning now trying to connect push and pull based systems, and I cannot buffer either as the ResultSet is stateful with each onNext() call. Is this an impossible task?

import rx.Observable
import rx.Subscriber
import java.sql.ResultSet

fun Observable<ResultSet>.asSequence() = object: Iterator<ResultSet>, Subscriber<ResultSet>() {

    private var isComplete = false

    override fun onCompleted() {
        isComplete = true
    }

    override fun onError(e: Throwable?) {
        throw UnsupportedOperationException()
    }

    override fun onNext(rs: ResultSet?) {
        throw UnsupportedOperationException()
    }


    override fun hasNext(): Boolean {
        throw UnsupportedOperationException()
    }

    override fun next(): ResultSet {
        throw UnsupportedOperationException()
    }

}.asSequence()
tmn
  • 11,121
  • 15
  • 56
  • 112
  • I've done something similar using a strictly pull-based implementation https://github.com/thomasnield/kdbc/blob/master/src/main/kotlin/kdbc/SelectBuilder.kt#L23:L38 – tmn May 24 '16 at 19:01
  • 1
    An observable can actually work in another thread, so I don't believe it's easy (if possible at all). Why avoid `toBlocking()`? As I understand, it would be a safe and easy way to achieve what you need. – hotkey May 24 '16 at 19:36
  • I don't think that would work with a stateful ResultSet... – tmn May 24 '16 at 21:39

2 Answers2

5

I'm not sure that's the easiest way to achieve what you want but you can try this code. It converts an Observable to an Iterator by creating a blocking queue and publishing all events from the Observable to this queue. The Iterable pulls events from the queue and blocks if there're none. Then it modify its own state depending on received current event.

class ObservableIterator<T>(
    observable: Observable<T>,
    scheduler: Scheduler
) : Iterator<T>, Closeable {

  private val queue = LinkedBlockingQueue<Notification<T>>()
  private var cached: Notification<T>? = null
  private var completed: Boolean = false

  private val subscription =
      observable
          .materialize()
          .subscribeOn(scheduler)
          .subscribe({ queue.put(it) })

  override fun hasNext(): Boolean {
    cacheNext()
    return !completed
  }

  override fun next(): T {
    cacheNext()
    val notification = cached ?: throw NoSuchElementException()
    check(notification.isOnNext)
    cached = null
    return notification.value
  }

  private fun cacheNext() {
    if (completed) {
      return
    }

    if (cached == null) {
      queue.take().let { notification ->
        if (notification.isOnError) {
          completed = true
          throw RuntimeException(notification.throwable)
        } else if (notification.isOnCompleted) {
          completed = true
        } else {
          cached = notification
        }
      }
    }
  }

  override fun close() {
    subscription.unsubscribe()
    completed = true
    cached = null
  }
}
Michael
  • 53,859
  • 22
  • 133
  • 139
  • Excellent. I'll play with this tonight when I get home. – tmn May 24 '16 at 21:37
  • Studying this on my phone this is brilliant. The only thing that might be of concern is the fact only one ResultSet should be in the queue at a time, and it should block on either the get or put end to ensure that. If the queue cannot be constrained, I could always create my own synchronizer... – tmn May 24 '16 at 21:52
  • 1
    @ThomasN. You can set maximum capacity to the `queue` passing it to a `LinkedBlockingQueue ` constructor. – Michael May 24 '16 at 22:05
  • 2
    How is this way different from `Sequence { observable.toBlocking().getIterator() }`? – Ilya May 25 '16 at 00:19
  • Ilya, the `toBlocking().getIterator()` seems to collect all items before iterating them. The `onCompleted()` gets called before any sequences are printed. `Observable.just("Alpha","Beta","Gamma").doOnCompleted { println("Done!") }.toBlocking().iterator.asSequence().forEach { println(it) }` – tmn May 25 '16 at 02:13
  • This is working beautifully @Michael. One question. Obviously some scheduling is needed to make two independent work streams, one for the `Observable` and another for the `Sequence`. But why the `observeOn()` and not a `subscribeOn()`? Is there any risk for backpressure problems? Or will the blocking on the queue keep that from happening? – tmn May 25 '16 at 02:27
  • Interesting. If I use `subscribeOn()` instead of `observeOn()`, the emissions and sequences interleave like I would want. But the `observeOn()` seems to stage and collect the emissions before moving on to the sequences. Are you sure this shouldn't use `subscribeOn()`? The `Observable` and `Sequence` operations must both be operating on completely separate threads. – tmn May 25 '16 at 02:56
  • @ThomasN. if you take a look at the implementation of `getIterator`, you'll find it very similiar to the answer given by @Michael: https://github.com/ReactiveX/RxJava/blob/1.x/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java – Ilya May 25 '16 at 03:06
  • 1
    You can ensure that `toBlocking().getIterator()` doesn't wait for observable to complete with this snippet: `Observable.interval(500, TimeUnit.MILLISECONDS) .take(10) .doOnCompleted { println("Done!") } .toBlocking().iterator.asSequence() .forEach { println(it) }` – Ilya May 25 '16 at 03:07
  • @ThomasN. Sorry, `observeOn` was just a bug. Fixed it. – Michael May 25 '16 at 05:12
  • @Ilya Seems you're right and I was tricked by docs. So I don't really see any difference now from what `.toBlocking().iterator` does. But it seems to me that my implementation doesn't do what @ThomasN. really wants. I mean it doesn't block producer until consumer is ready. Maybe I have to delete the answer. I'll try to do a little more research on this. – Michael May 25 '16 at 06:02
  • Just feel free to edit :) this got me closer than anything else. I can make my own synchronizer and drop it into this. – tmn May 25 '16 at 10:35
3

You can use the following helper function:

fun <T> Observable<T>.asSequence() = Sequence { toBlocking().getIterator() }

The observable will be subscribed to when the sequence returned is called for iterator.

If an observable emits elements on the same thread it was subscribed to (like Observable.just for example), it will populate the buffer of the iterator before it gets a chance to be returned. In this case you might need to direct subscription to the different thread with a call to subscribeOn:

observable.subscribeOn(scheduler).asSequence()

However, while toBlocking().getIterator() doesn't buffer all results it could buffer some of them if they aren't consumed timely by the iterator. That might be a problem if a ResultSet gets somehow expired when the next ResultSet arrives.

Ilya
  • 21,871
  • 8
  • 73
  • 92
  • Okay, maybe that is why I got the wrong impression as I needed to use subscribeOn(). I will take a look at this a little more closely tomorrow. I am glad there is an operator built in for this purpose. – tmn May 25 '16 at 03:29
  • 1
    @ThomasN. What do you mean by strictly sequential? If you must not return from `onNext` of the subscriber until the result set is fetched from the iterator and processed, I'm afraid you'll need some manual synchronization. – Ilya May 25 '16 at 03:41
  • Yes, that would be a problem with the ResultSet expiring. I believe the same ResultSet instance is emitted every time. It is its internal state that changes every time it is emitted. If its state is changing faster than each state can be consumed, that is a big problem. – tmn May 25 '16 at 03:42
  • Yeah, I think manual synchronization will be necessary so ill have to go with that similar implementation from @Michael. But thank you, this solution works for every other case I came across other than the ResultSet one :) – tmn May 25 '16 at 03:43
  • Got some help creating a synchronizer for this purpose here. http://stackoverflow.com/questions/37446814/creating-a-singleblockingqueue-synchronizer/37455141#37455141 – tmn May 26 '16 at 15:18