I have a list of completables that by default I run them one after one with concat/andThen operators.
Sometimes I want some part of the completables to run in parallel and after everything complete continue to the next completable in the list.
I tried to achieve that with this code:
var completable =
getAsyncCompletables()?.let {
it
} ?: run {
completables.removeAt(0).getCompletable()
}
while (completables.isNotEmpty()) {
val nextCompletable = getAsyncCompletables()?.let {
it
} ?: run {
completables.removeAt(0).getCompletable()
}
completable = nextCompletable.startWith(completable)
}
completable
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe()
I use this code to detect the async completables:
private fun getAsyncCompletables(): Completable? {
if (completables.size < 2 || !completables[1].async) {
return null
}
var completable = completables.removeAt(0).getCompletable()
while (completables.isNotEmpty() && completables[0].async) {
completable = completable.mergeWith(completables.removeAt(0).getCompletable())
}
return completable
}
All works fine, except one thing, the last completable not triggered althought I used "startWith".
I also tried "concatWith" and "andThen",but same result.
It is a bit difficult to answer without seeing more of your code, specifically what async does and what the data structure is for completables. However, the answer you are looking for is most likely similar regardless of these values. You will probably want to use Completable.merge(...) or Completable.mergeArray(...).
As per the documentation:
/**
* Returns a Completable instance that subscribes to all sources at once and
* completes only when all source Completables complete or one of them emits an error.
* ...
*/
In order to achieve the parallel execution, you will need to call subscribeOn each of your Completables in the list/array/set with a new thread. This can be done with Schedulers.newThread() or from a shared pool like Schedulers.io().
I ran a test just to be sure. Here is the code.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
val completeOne = Completable.fromAction {
Timber.d("Completable #1 running on ${Thread.currentThread().name}")
}
val completeTwo = Completable.fromAction {
Timber.d("Completable #2 running on ${Thread.currentThread().name}")
}
val completeThree = Completable.fromAction {
Timber.d("Completable #3 running on ${Thread.currentThread().name}")
}
val completables = listOf(completeOne, completeTwo, completeThree).map { CompletableWrapper(it) }
val asyncCompletables = completables
.asSequence()
.filter { it.async }
.map { it.getCompletable().subscribeOn(Schedulers.io()) }
.toList()
Completable.merge(asyncCompletables)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Timber.i("Completed all completables")
}, Timber::e)
}
class CompletableWrapper(
private val completable: Completable,
val async: Boolean = true
) {
fun getCompletable() = completable
}
And here is the output.
D/MainActivity$onCreate$completeThree: Completable #3 running on RxCachedThreadScheduler-3
D/MainActivity$onCreate$completeTwo: Completable #2 running on RxCachedThreadScheduler-2
D/MainActivity$onCreate$completeOne: Completable #1 running on RxCachedThreadScheduler-1
I/MainActivity$onCreate: Completed all completables
As you can see, it runs each completable on a new thread from the pool and only calls completed all after each completable has finished.
See here for the documentation on Completable.merge/mergeArray.
Related
I'm trying to unit test my viewmodel code, which does a server polling to return data as soon as its Status == ELIGIBLE
My problem is, it always assert when it's still loading (repeating), and not waiting for the onSuccess to be called to assert the correct status.
I've put some logs to track what's happening:
doOnSubscribe called
repeatWhen called
doOnNext called
takeUntil called
doOnNext called
takeUntil called
As you can see, repeatWhen and takeUntil are called twice (which is expected), but after that, no onSuccess called.
And eventually the test fails with this message
Caused by: java.lang.AssertionError: expected:<SUCCESS> but was:<LOADING>
If I removed the failing line, the next assertion would fail too with message:
java.lang.AssertionError:
Expected :ELIGIBLE
Actual :null
Which mean the onSuccess method is not yet reached, and is still loading.
I also don't prefer using Schedulers.trampoline() .. it works, but it waits for 5 secs synchronously. I prefer to use TestScheduler.advanceByTime() instead.
Here's the client code:
fun startStatusPolling() {
val pollingSingle = shiftPayService.obtainCardStatus()
.repeatWhen {
println("repeatWhen called")
//POLLING_INTERVAL_SECONDS = 5
it.delay(POLLING_INTERVAL_SECONDS, TimeUnit.SECONDS)
}
.takeUntil { item ->
println("takeUntil called")
item.cardStatus != Status.PENDING
}.doOnNext {
println("doOnNext called")
}
.lastElement()
.toSingle()
subscribe(pollingSingle, pollingStatusLiveData)
}
And my test class:
#RunWith(HomebaseRobolectricTestRunner::class)
#LooperMode(LooperMode.Mode.PAUSED)
class CardViewModelTest {
lateinit var viewModel: CardViewModel
var testScheduler = TestScheduler()
#Before
fun setup() {
RxJavaPlugins.setComputationSchedulerHandler { testScheduler }
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { testScheduler }
val cardStatusPending: CardStatus = mockk(relaxed = true) {
every { status } returns Status.PENDING
}
val cardStatusEligible: CardStatus = mockk(relaxed = true) {
every { status } returns Status.ELIGIBLE
}
val cardService: CardService = spyk {
every { obtainCardStatus() } returnsMany listOf(
Single.just(cardStatusPending),
Single.just(cardStatusEligible)
)
}
viewModel = CardViewModel(cardService)
}
#Test
fun testCardStatusPolling() {
viewModel.startStatusPolling()
shadowOf(Looper.getMainLooper()).idle()
testScheduler.advanceTimeBy(5, TimeUnit.SECONDS)
//after 5 sec delay, single is resubscibed, returning the second single cardStatusEligible
assertEquals(Result.Status.SUCCESS, viewModel.pollingStatusLiveData.value?.status)
assertEquals(EligiblityStatus.ELIGIBLE, viewModel.pollingStatusLiveData.value?.data?.eligibilityStatus)
}
}
I will be using thread executors to do some background work with rxkotlin, I made threadpool size fixed to 3, but my problem is during my background operation it using only one thread out of 3, which slows down my background operation
Executor class
class ThreadExe : Executor{
companion object {
private const val THREAD_POOL_SIZE = 3
}
private val executor: Executor =
Executors.newFixedThreadPool(THREAD_POOL_SIZE)
override fun execute(runnable: Runnable) {
executor.execute(runnable)
}
}
The above is my executor class responsible for creating thread.
I will be calling my background task like below
getSomeDataFromNetworkProcessWithDB()
.subscribeOn(Schedulers.from(ThreadExe()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
fun getSomeDataFromNetworkProcessWithDB() {
Observable.fromIteratable(someDataList())
.flatMap {
getSomeNetworkData()
}
.flatMap {
doSomeDbOperation()
}
}
my problem with the above code is all these network and db operation is working sequentially with the same thread, since we have give the threadpool of size 3 it must send the 3 network request parallely, but the request is going sequentially
Can anyone help me out this problem ?
If you want individual operation to run on different thread try this:
getSomeDataFromNetworkProcessWithDB(Schedulers.from(ThreadExe()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
fun getSomeDataFromNetworkProcessWithDB(scheduler: Scheduler): Observable<Data> {
return Observable.fromIterable(someDataList())
.flatMap {
getSomeNetworkData().subscribeOn(scheduler)
}
.flatMap {
doSomeDbOperation().subscribeOn(scheduler)
}
.subscribeOn(scheduler) // optional, if you want fromIterable(), someDataList() to run on this scheduler.
}
I currently have an EditText for the user to enter a search. I'm trying to use RxJava with debounce to only search every so often, instead of each character. However, I'm getting an InterruptedIOException while I'm testing, which kills the stream.
private val subject = BehaviorSubject.create<String>()
init {
configureAutoComplete()
}
private fun configureAutoComplete() {
subject.debounce(200, TimeUnit.MILLISECONDS)
.flatMap {
getSearchResults(query = it)
}
.subscribe({ result ->
handleResult(result)
}, { t: Throwable? ->
Logger.e(t, "Failed to search")
})
}
fun getSearchResults(query: String): Observable<List<MyObject>> {
val service = NetworkService.create() // get retrofit service
return service.search(query)
}
fun search(text: String) {
subject.onNext(text)
}
As you can see, I'm creating a BehaviorSubject, and within init I'm setting it up with debounce.
getSearchResult returns an Observable and does my network request.
But as I'm testing, if I type at a specific rate ( usually quick-ish, like typing another character while the request is ongoing ) it'll throw an Exception.
Failed to search : java.io.InterruptedIOException
at okhttp3.internal.http2.Http2Stream.waitForIo(Http2Stream.java:579)
at okhttp3.internal.http2.Http2Stream.takeResponseHeaders(Http2Stream.java:143)
at okhttp3.internal.http2.Http2Codec.readResponseHeaders(Http2Codec.java:125)
I was looking at this, https://stackoverflow.com/a/47276430/3106174, and it seems like I'm doing everything correctly.
After more testing, I realized that the network request was on the main thread.
You can test this by replacing your network call with Observerable.create{ ... } and throwing a Thread.sleep(1000) inside.
I was following this tutorial, https://proandroiddev.com/building-an-autocompleting-edittext-using-rxjava-f69c5c3f5a40, and one of the comments mention this issue.
"But I think one thing is misleading in your code snippet, and it’s
that subjects aren’t thread safe. And the thread that your code will
run on will be the thread that you emitting on (in this case the main
thread). "
To solve this issue, you need to force it to run on Schedulers.io(). Make sure it's after the debounce or it won't work.
private fun configureAutoComplete() {
subject.debounce(200, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.io()) // add this here
.distinctUntilChanged()
.switchMap {
getSearchResults(query = it)
}
.subscribe({ result ->
handleResult(result)
}, { t: Throwable? ->
Logger.e(t, "Failed to search")
})
}
I want to create worker queue using RxJava: I have a single thread doing some work, and I want to guarantee that no other job will be executed until we have finished/failed the current job.
My solution is simply to block the observable and wait for the result:
fun foo() : Observable<Foo> {
return Observable.unsafeCreate { subscriber ->
handlerThread.post {
val answer = object.performSomeJob(whatever)
.flatMap { object.performAnotherJob(whatever) }
.flatMap { object.performLastJob(whatever) }
.blockingFirst()
subscriber.onNext(answer)
subscriber.onComplete()
}
}
}
You may argue that there is no need to use RxJava since everything's synchronous. That's true for this particular method, but:
I want to avoid 'callback hell': there are three methods, each of which is taking callback and I use RxJava to chain them
I use Rx further on in the caller method.
I know that blocking is generally considered as an anti-pattern, so can I do better in my case?
you can use concat to perform work sequentially on some thread:
fun foo(): Observable<Foo> {
return performSomeJob(whatever)
.concatMap { performAnotherJob(whatever) }
.concatMap { performLastJob(whatever) }
.subscribeOn(Schedulers.newThread())
}
You can schedule all your work on one single-threaded Scheduler such as
#NonNull
public static Scheduler single()
Returns a default, shared, single-thread-backed Scheduler instance for work requiring strongly-sequential execution on the same background thread.
fun foo(): Observable<Foo> =
Observable.fromCallable { object.performSomeJob(whatever) }
.subscribeOn(Schedulers.single())
.observeOn(Schedulers.single())
.flatMap { object.performAnotherJob(whatever) }
.flatMap { object.performLastJob(whatever) }
I am trying to chain few observables together and do some action depending what observable has been executed. But I faced with strange behavior.
class MainActivity : AppCompatActivity() {
val TAG: String = MainActivity::class.java.name
private lateinit var clicker: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
clicker = findViewById(R.id.clicker) as TextView
clicker.setOnClickListener {
val i = AtomicInteger()
getFirstObservable()
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
showMessage(i, it)
}
.flatMap { getSecondObservable() }
.doOnNext {
showMessage(i, it)
}
.flatMap { getThirdObservable() }
.doOnNext {
showMessage(i, it)
}
.subscribe()
}
}
fun getFirstObservable(): Observable<String> {
return Observable.fromCallable {
Thread.sleep(2000)
"Hello"
}
}
fun getSecondObservable(): Observable<Int> {
return Observable.fromCallable {
Thread.sleep(2000)
3
}
}
fun getThirdObservable(): Observable<String> {
return Observable.fromCallable {
Thread.sleep(2000)
"World!"
}
}
fun showMessage(i: AtomicInteger, obj: Any) {
val msg = "Message #${i.incrementAndGet()}: from ${Thread.currentThread().name}: $obj"
Log.e(TAG, msg)
clicker.text = msg
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
In this example logs would be shown every 2 seconds but all changes with views would be done when the last observable will be finished.
12-04 01:11:30.465 19207-19207/com.googlevsky.rxtest E/com.googlevsky.rxtest.MainActivity: Message #1: from main: Hello
12-04 01:11:32.473 19207-19207/com.googlevsky.rxtest E/com.googlevsky.rxtest.MainActivity: Message #2: from main: 3
12-04 01:11:34.479 19207-19207/com.googlevsky.rxtest E/com.googlevsky.rxtest.MainActivity: Message #3: from main: World!
I think it is behavior of AndroidScheduler.mainThread(), because when I remove this line and wrap changes with views like this
Handler(Looper.getMainLooper()).post {
clicker.text = msg
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
behavior becomes right. So can someone explain this behavior and suggest a correct way to solve this problem?
Most of your code is being executed on the the main thread, including the sleeps. When an observable is created, it is subscribed on and observed on the current thread unless otherwise specified. When you create your second and third observables, they are on the main thread. Furthermore, since there is no backgrounding of the observable work, it is executed immediately on the current thread when you subscribe. Therefore, all the work and observations happen on the main thread without yielding back to the android OS. The UI is blocked waiting for time on the main thread. If you increase those sleep times, you can force an ANR. To fix it, you can specify observeOn and subscribeOn for each of your observables to push the work to a computation thread for each of them.
getFirstObservable().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
showMessage(i, it)
}
.flatMap {
getSecondObservable().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
}
.doOnNext {
showMessage(i, it)
}
.flatMap {
getThirdObservable().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
}
.doOnNext {
showMessage(i, it)
}
.doOnNext {
showMessage(i, it)
}
.subscribe()