Repository navigation
Closed
Milestone
Description
There appears to be a bug, or at the very least an undocumented and unexpected case, where FlowablePublish causes upstream operators to be applied on the subscription scheduler rather than the upstream observation scheduler (due to polling). This is reproducible at least in 2.1.x and 2.2.0.
Sample (Kotlin)
val scheduler1 = Schedulers.from(Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "scheduler1")
})
val scheduler2 = Schedulers.from(Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "scheduler2")
})
val flowable: Flowable<Long> = Flowable.interval(1L, TimeUnit.MILLISECONDS)
.onBackpressureLatest()
.take(10_000)
.observeOn(scheduler1)
.map {
val threadName = Thread.currentThread().name
if (threadName != "scheduler1") {
throw AssertionError("Wrong thread: $threadName")
}
it
}
.share()
flowable
.observeOn(scheduler2)
.doOnNext { Thread.sleep(10L) }
.subscribe {
println("onNext on ${Thread.currentThread().name}: $it")
}Expected Behavior
It should be expected that the .map operator is applied on the scheduler1 scheduler since it is the scheduler supplied to observeOn immediately upstream.
Actual Behavior
After about ~100 emissions, the map operator is applied on the scheduler2 scheduler. I'm not an expert in the details, but it seems to have something to do with backpressure, buffering, and polling for items upon subscription?
Output
onNext on scheduler2: 0
onNext on scheduler2: 1
onNext on scheduler2: 2
onNext on scheduler2: 3
onNext on scheduler2: 4
onNext on scheduler2: 5
onNext on scheduler2: 6
onNext on scheduler2: 7
onNext on scheduler2: 8
onNext on scheduler2: 9
onNext on scheduler2: 10
onNext on scheduler2: 11
onNext on scheduler2: 12
onNext on scheduler2: 13
onNext on scheduler2: 14
onNext on scheduler2: 15
onNext on scheduler2: 16
onNext on scheduler2: 17
onNext on scheduler2: 18
onNext on scheduler2: 19
onNext on scheduler2: 20
onNext on scheduler2: 21
onNext on scheduler2: 22
onNext on scheduler2: 23
onNext on scheduler2: 24
onNext on scheduler2: 25
onNext on scheduler2: 26
onNext on scheduler2: 27
onNext on scheduler2: 28
onNext on scheduler2: 29
onNext on scheduler2: 30
onNext on scheduler2: 31
onNext on scheduler2: 32
onNext on scheduler2: 33
onNext on scheduler2: 34
onNext on scheduler2: 35
onNext on scheduler2: 36
onNext on scheduler2: 37
onNext on scheduler2: 38
onNext on scheduler2: 39
onNext on scheduler2: 40
onNext on scheduler2: 41
onNext on scheduler2: 42
onNext on scheduler2: 43
onNext on scheduler2: 44
onNext on scheduler2: 45
onNext on scheduler2: 46
onNext on scheduler2: 47
onNext on scheduler2: 48
onNext on scheduler2: 49
onNext on scheduler2: 50
onNext on scheduler2: 51
onNext on scheduler2: 52
onNext on scheduler2: 53
onNext on scheduler2: 54
onNext on scheduler2: 55
onNext on scheduler2: 56
onNext on scheduler2: 57
onNext on scheduler2: 58
onNext on scheduler2: 59
onNext on scheduler2: 60
onNext on scheduler2: 61
onNext on scheduler2: 62
onNext on scheduler2: 63
onNext on scheduler2: 64
onNext on scheduler2: 65
onNext on scheduler2: 66
onNext on scheduler2: 67
onNext on scheduler2: 68
onNext on scheduler2: 69
onNext on scheduler2: 70
onNext on scheduler2: 71
onNext on scheduler2: 72
onNext on scheduler2: 73
onNext on scheduler2: 74
onNext on scheduler2: 75
onNext on scheduler2: 76
onNext on scheduler2: 77
onNext on scheduler2: 78
onNext on scheduler2: 79
onNext on scheduler2: 80
onNext on scheduler2: 81
onNext on scheduler2: 82
onNext on scheduler2: 83
onNext on scheduler2: 84
onNext on scheduler2: 85
onNext on scheduler2: 86
onNext on scheduler2: 87
onNext on scheduler2: 88
onNext on scheduler2: 89
onNext on scheduler2: 90
onNext on scheduler2: 91
onNext on scheduler2: 92
onNext on scheduler2: 93
onNext on scheduler2: 94
onNext on scheduler2: 95
io.reactivex.exceptions.OnErrorNotImplementedException: Wrong thread: scheduler2
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704)
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701)
at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:79)
at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:207)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:392)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker$BooleanRunnable.run(ExecutorScheduler.java:261)
at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker.run(ExecutorScheduler.java:226)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.AssertionError: Wrong thread: scheduler2
at RxTestKt$main$flowable$1.apply(RxTest.kt:22)
at RxTestKt$main$flowable$1.apply(RxTest.kt)
at io.reactivex.internal.operators.flowable.FlowableMap$MapSubscriber.poll(FlowableMap.java:81)
at io.reactivex.internal.operators.flowable.FlowablePublish$PublishSubscriber.dispatch(FlowablePublish.java:510)
at io.reactivex.internal.operators.flowable.FlowablePublish$InnerSubscriber.request(FlowablePublish.java:615)
at io.reactivex.internal.operators.flowable.FlowableRefCount$RefCountSubscriber.request(FlowableRefCount.java:216)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:407)
... 6 more
Exception in thread "scheduler2" io.reactivex.exceptions.OnErrorNotImplementedException: Wrong thread: scheduler2
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704)
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701)
at io.reactivex.internal.subscribers.LambdaSubscriber.onError(LambdaSubscriber.java:79)
at io.reactivex.internal.operators.flowable.FlowableDoOnEach$DoOnEachSubscriber.onError(FlowableDoOnEach.java:111)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.checkTerminated(FlowableObserveOn.java:207)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:392)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$BaseObserveOnSubscriber.run(FlowableObserveOn.java:176)
at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker$BooleanRunnable.run(ExecutorScheduler.java:261)
at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker.run(ExecutorScheduler.java:226)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.AssertionError: Wrong thread: scheduler2
at RxTestKt$main$flowable$1.apply(RxTest.kt:22)
at RxTestKt$main$flowable$1.apply(RxTest.kt)
at io.reactivex.internal.operators.flowable.FlowableMap$MapSubscriber.poll(FlowableMap.java:81)
at io.reactivex.internal.operators.flowable.FlowablePublish$PublishSubscriber.dispatch(FlowablePublish.java:510)
at io.reactivex.internal.operators.flowable.FlowablePublish$InnerSubscriber.request(FlowablePublish.java:615)
at io.reactivex.internal.operators.flowable.FlowableRefCount$RefCountSubscriber.request(FlowableRefCount.java:216)
at io.reactivex.internal.operators.flowable.FlowableObserveOn$ObserveOnSubscriber.runAsync(FlowableObserveOn.java:407)
... 6 more
Reacted by Sam Thompson and Dave Moten
Activity
akarnokd commentedon Aug 7, 2018
This a bug due to operator fusion breaking the thread-confinement expected after
observeOn. You can work around it by applyinghide()beforeshare(). I'll post a fix shortly.damianw commentedon Aug 13, 2018
@akarnokd I've noticed that
.hide()does not always solve the problem - adding a secondmap(and/or probably other operators) after thesharecauses the problem to reappear:Output:
I have two questions about this:
akarnokd commentedon Aug 13, 2018
No, this is a different effect. Due to backpressure and buffering, the
publishmay service its consumer on a different thread than the generator upstream to it.If you want that
mapto execute on the desired thread, you have to applyobserveOn(scheduler1)before it.damianw commentedon Aug 13, 2018
Thanks for the reply, @akarnokd. That makes sense - I will keep that in mind. Are there any existing mechanisms that allow something like
publishto preserve the observation scheduling?akarnokd commentedon Aug 13, 2018
No. It is an async boudary in which case all bets are off. You have to ensure the proper thread via observeOn.