Moya: Request method from RxMoyaProvider modifies observable's scheduler

Created on 10 Mar 2016  ·  30Comments  ·  Source: Moya/Moya

Hello guys, I need that the Observable be observed on a background thread, I know this is no the right way to do it but this is my approach:

https://gist.github.com/robertofrontado/475787b538d3d4c9f90d

From the RxSwift documentation: https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Schedulers.md

"In case observeOn isn't explicitly specified, work will be performed on which ever thread/scheduler elements are generated."

So when I received an Observable form Moya, which has been subscribed on (subscribeOn) a background thread, I'll expect that the whole observable chain (map, flatMap...) will be performed in that thread (not the main), until I change it eventually using observeOn

Thanks

Most helpful comment

@beretis We added this extension, and using it every time we use moya

// Fixed Moya scheduler issue
extension RxMoyaProvider {

    func requestBackground(target: Target) -> Observable<Response> {
        return request(target).observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
    }
}

like provider.requestBackground({{Target}}), so with .observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background)) we force the Observable chain to perform the api call on background

All 30 comments

Hi there! I want to clarify what you're asking for – are you suggesting a change to Moya's behaviour? What change are you looking for?

As far as I know, subscribeOn isn't typically used, because its the observation that matters and its the observation that has the side-effects (not the subscription). Does that make sense?

Not the behaviour itself, what I'm suggesting is that I want to perform some operations, such map or flatMap, in the same Scheduler (background thread) which I'm subscribing it (subscribeOn). But Moya seems to not to follow the convention declared on RxSwift docs:

In case observeOn isn't explicitly specified, work will be performed on which ever thread/scheduler elements are generated.

So the observable do not observe (map, flatmap) in the thread specified when calling subscribeOn

Hi @ashfurrow!

I think subscribeOn matters (or should do it). Take for example the changed performed in Retrofit 2 (after released beta-2).

Fix: Observable and Single-based execution of requests now behave synchronously (and thus requires subscribeOn() for running in the background).

They changed this because the library was modifying the thread on which the request was performed. And this responsibility should belong to the users of the library, not the library itself. I think Moya should do the same, because in the end is the user who consumes the observable who will know better in which thread subscribe or observe.

Nevertheless, I think the issue reported by @robertofrontado is related to this, but it is not the problem itself.

It seems to me like the underlying request performed by Alamofire is changing the thread to the Main thread, after the callback request is called. But I’m just guessing here.

Anyway, thanks for this amazing library!

I guess I must be confused about the issue then – I was thinking Moya returns an Observable and the user can do whatever they like with it. Alamofire sends callbacks on the main thread, and we simply send them through.

To clarify, is the distinction one between Moya allowing users to specify a scheduler to observe/subscribe on versus requiring users to call observeOn/scheduleOn themselves?

In any case, I think keeping the observe/subscribe schedulers as the main thread scheduler _by default_ is the safest move, since users will often update UI elements based on the callback. Actually, I'm kind of curious why you would want the callbacks on a separate thread for all requests – is it a performance issue?

Let me know what you think – thanks for taking the time to clarify for me, I want to make sure I understand the problem fully before we make any breaking code changes :wink:

That’s the point :)

I'm kind of curious why you would want the callbacks on a separate thread for all requests – is it a performance issue?

Yes, because I usually chain multiple operations after the request -using map or flatmap, like persisting data in disk or whatever.

But I’m not asking Moya to behave like that. I mean, _I don’t wanna the callbacks on a separate thread per se, I just want to Moya does not change the thread which I choose when using subscribeOn and ObserveOn_.

Because Moya sticks to the Alamofire procedure on threading synchronization (which makes a lot of sense outside the rx world), Moya does not follow the standards of reactive family.

Like @robertofrontado pointed out before:

In case observeOn isn't explicitly specified, work will be performed on which ever thread/scheduler elements are generated.

So, to sum up, I think Moya rx version should not set the thread on which to observe, neither the thread on which to subscribe. Moya should respect in which thread the user choose to be performed the requested, by modifying internally nothing. Moya should not call internally observerOn or subscribeOn (plus Moya should take care of that synchronisation issue related with the fact that Alamofire synchronizes callbacks to the main thread by default).

But this is my opinion. And of course, yes, it could potentially break someone’s code. Retrofit broke mine when I upgraded to the last version. But it was for the better, because that way I was able to understand deeper the topic of scheduling.

I just want to Moya does not change the thread which I choose when using subscribeOn and ObserveOn.

Right, that makes sense. I guess what I'm confused by is, as far as I know, Moya doesn't do that does it? If it does, can you specify where in the code it does?

The issue happens in the class RxMoyaProvider.

Observable starts to work in a background thread. That’s right:

background_thread

But after the callback, the actual thread becomes the Main thread:

main_thread

And after that point, every observable operation (flatmap, map, subscribe) will be performed in the main thread, unless I call explicitly obserberOn to observe in a particular thread.

But that's how Rx is supposed to work, no? The elements (network response) are made always on the main thread. If you want to observe them on a background thread, you should use observeOn, not subscribeOn (which I pointed out is rarely used).

I don't think so.

Rx knows nothing about the underlying task executed. Rx just knows that this "anonymous" task will be performed in some thread thanks to the configuration made to observable's state using subscribeOn and obserberOn.

And, as the official docs points out, subscribeOn specifies the Scheduler on which an Observable will operate.

So, if I subscribeOn in a background thread, and I never specify in what thread the observable should observe, the convention is to observe in the thread specified when calling subscribeOn. But Moya changes that, and it is due to the underlying task of Alamofire -or at least I think that's the problem.

I'm basing my assumptions of how subscribeOn: works off of the RxSwift-specific documentation, which says:

Wraps the source sequence in order to run its subscription and unsubscription logic on the specified scheduler.

This operation is not commonly used.

This only performs the side-effects of subscription and unsubscription on the specified scheduler.

In order to invoke observer callbacks on a scheduler, use observeOn.

Which sounds like it differs from the general Rx feel. I've contacted the RxSwift maintainers to ask for a clarification.

I don't see any different. I think it says the same thing. Indeed, you can create an observable with RxSwift and call subscribeOn and you will see that it behaves like already told you.

The real problem is that Moya is changing the scheduler in the middle of the task which performs the observable. Like I pointed out before, with the screenshot.

But we can wait to see what they have to say about that. Maybe I'm wrong after all ;)

OK, let's back up a little here. This quote:

In case observeOn isn't explicitly specified, work will be performed on which ever thread/scheduler elements are generated

Has nothing to do with which scheduler Moya is called from, it _only_ has to do with Alamofire. Unless you specify which scheduler, the work (map, flatMap, subscribeNext, etc) will be performed on whichever scheduler the elements are _generated_ on. Alamofire always generates these on the main thread, and Moya just passes them through, so they go on the main thread scheduler.

I think you're asking for the following: if a network request is made on a background thread, then its response should be delivered on that same background thread, but that's not how RxSwift works. RxSwift observables generate elements from whatever scheduler/thread that they want, and it's up to you to observe them on a separate scheduler with observeOn:, otherwise RxSwift conventions specify it's up to Moya, and Alamofire, to decide whichever thread/scheduler they want since they are the ones generating the elements.

I hope that makes sense. If it doesn't, I'm happy to schedule a call with you and talk this out.

Don’t worry :) I can see your point.

It’s just that for me I see it from the other side. I’m a user of Moya, and I expect that its tasks would be performed in whatever scheduler I would set calling onSubscriber. I think Moya should give complete freedom to the user, not forcing the scheduler to be the Main thread, just because Alamofire do that. It’s something common for no rx users, but I think most rx people would expect full control over the observable’s threading.

Thanks anyway!

I’m a user of Moya, and I expect that its tasks would be performed in whatever scheduler I would set calling onSubscriber

But Moya _does_ do this, like any Rx Observable. Just because it omits things on the main thread scheduler doesn't mean you can't shunt those to another scheduler. I'd encourage you to re-read the RxSwift observeOn function documentation – it's the method that you should be calling to put observables onto other schedulers.

With observeOn I can't change in which scheduler Moya performs its tasks. I just can change it using subscribeOn. In fact, it changes it until the code execution hits the 22 line, when unexpectedly -for me, as a user- Moya changes its scheduler.

This is typical behaviour in Rx; it sounds like you're looking for Moya to emit events on the scheduler you call request() from, but that's not how Rx works. You _do_ have control over the observables scheduling but only from the outside, which is again very typical. If you want the events to be observed on another scheduler, you have to call observeOn.

@ashfurrow is right. In the Rx world, Moya's behaviour is correct. There's a fundamental misconception on the first assumption that seems to be the root of this discussion and is Scheduler = Thread. In a first instance I got tricked as well, but then re-reading the whole discussion I understood what the original issue was about and I can say that Moya is behaving in a more than reasonable way.

The fact that is changing the thread is something natural if considering that Moya is a helper to talk with an api, perform the work in the background and call the callback in the main scheduler to update the UI in most of the times. Changing Moya's behaviour would be more a design decision, rather than a real solution for a bug or the necessity to comply to a guideline.

I would also like to point to the chapter about Schedulers in introtorx.com:

Being free-threaded means that you are not restricted to which thread you choose to do your work. For example, you can choose to do your work such as invoking a subscription, observing or producing notifications, on any thread you like.

We should remember that in the Rx world there's no thread concept, considering its "threat agnostic" nature, but we have the scheduler abstraction that is the entity taking care of scheduling the work.

The referenced documentation is more for RxSwift itself, rather than for a third party library relying on it. So the discussion about how Moya should behave, has to be done with @ashfurrow, in my opinion it shouldn't be changed, but I am happy to follow the discussion and help if necessary.

Hi @bontoJR :)

If you think of Moya like a wrapper of Alamofire, it makes sense that Moya returns an observable which by default observe on the Main thread. But we are talking about the rx extension part of Moya, which I don’t see anymore like a wrapper for Alamofire. Instead, I see it just like a rx resource which performed certain task. So if I don’t specify any scheduler, I would expect -just like any other observable, its task will be performed in the same scheduler-thread from where I’m calling it.

But that’s only my opinion.

Hello @VictorAlbertos. I really like this part of your post: I see it just like a rx resource which performed certain task, you're right, RxMoya is a resource. I just added my 2 cents because the original documentation for RxSwift has been referenced, alongside the ReactiveX official documentation, to justify and promote a change in the default behaviour of RxMoya, which I think they should not be related. RxMoya is using RxSwift, but it's not part of the original RxSwift framework. :)

I see your point and I would definitely agree in case the error would have been in the RxSwift code, but in this case it's a matter of design specific decision rather than a design error. I don't see any issue keeping the current behaviour, but I would like to know in which case you would prefer to have the callback called in a scheduler working in a background thread, rather than the main one, just to better picture a potential scenario and see if the change would add more value than problems. :)

Of course!

We are (@robertofrontado and me) just finishing the port of this android base project to the iOS ecosystem. It splits the responsibilities into 3 layers. Data, domain and presentation. Every operation performed in the data layer is an i/o operation. The data retrieved from Moya needs to be cached eventually using RxCache (I’ll release the port for iOS next week and I think it will be work almost (xD) perfectly with Moya and ObjectMapper), so the next actions upon the observable chain keep being i/o operations. So it does not make sense to me that Moya, which is a library focused on i/o operations, suddenly changes the current thread to the main thread, especially if I’ve called subscribeOn to ensure that the underlying task -as such the observable operations, will be performed in another thread.

This puristic complain has also a practical implication xD

In the domain layer resides the base class Presenter. This method schedulers specify the Scheduler on which the observable coming from the data layer (Moya) will operate using an Scheduler of type i/o, and its items emitted will be observed in the UI thread. But only when the observable reaches this point in the chaining operation, not before! And because Moya changes this (I think standard behaviour in rx ecosystem) the scheduling configuration is messed up and now it is required to be called obserbeOn in the data layer in order to prevent this -for me, again- unexpected behaviour.

OK, so I think we've discussed this thoroughly and have to come a conclusion that the behaviour Moya is exhibiting is idiomatic to RxSwift, though may be unexpected in a thread-based context (as Junior said, Rx is thread-agnostic). I'm going to close the issue, but if anyone has follow-up, feel free to comment, re-open, or open a new issue :cake:

I think I understand the two sides of this idea:

  1. The ability to use RxMoya without Alamofire dispatching to a different thread
  2. The fact that ^ is the expected behavior for a majority of apps using (Rx)Moya, to load a network resource and then show it in the UI

I was wondering how to get closer to the first scenario - strictly as a client of this library, not to change the implementation inside RxMoya.

Is it as simple as calling .observeOn(ABackgroundThreadForMoreProcessing.instance) after my .flatMap { provider.request(.zenResource) }?
So something like

provider.request(.networkResource) // called on whatever the current thread is
    .observeOn(ABackgroundThreadForMoreProcessing.instance) 
    .flatMap(persistToLocalStorage) // called on the thread associated with ABackgroundThreadForMoreProcessing
    .flatMap(expensiveConversionToMakePresentableToUI) // called on the thread associated with ABackgroundThreadForMoreProcessing
    .observeOn(MainScheduler.instance)
    .subscribe { /* bind the result to UI here */ } // called on the main thread

Also, maybe it would be cool to expose https://github.com/Alamofire/Alamofire#response-handler-queue through RxMoya? That way, as a Rx user, if I explicitly say .observeOn a specific scheduler, we can pass the queue related to that scheduler to Alamofire?

EDIT: it looks like somebody had a similar question about doing the same with Alamofire https://github.com/Alamofire/Alamofire/issues/1147 (another use case that involved Rx) - may be worth adding an optional parameter to the provider that specifies which queue to run the responseHandler on 👀

Hmm, interesting. To answer your question:

Is it as simple as calling ...

Yup, I've done this and it's awesome how simple it is. I'm up for including this as an optional parameter, should it go on the provider subclass initializer or on the request method?

That is so awesome ❤️

I'll also go ahead and open a PR to track progress on the optional param.

My thoughts on where to put the param is definitely impacted by what I'm currently working on though: I'm moving an SDK (almost) completely onto a background thread, so all my requests are going to be moved onto a background thread, and I'm going to call .observeOn(MainScheduler.instance) after I've parsed and cached the data and to pass it to a client

It makes more sense to me as in the provider subclass - so I have consistent behavior across all my requests, but I can be convinced otherwise. What do you think?

Makes sense to put it in the subclass/initializer 👍

Created #762 🙃

Hi guys, I'm sorry for opening this again but I think my network calls are happening in UI Thread. I'm experiencing UI freezes when performing calls, so I started to dig around, I found this issue and noticed that on @VictorAlbertos 's screenshots RxMoyaProvider's request function starts in thread 4 and then switch to Main thread. This doesn't happen in my case. Everithing happens in UI thread and I really don't know why.
screen shot 2017-01-19 at 17 16 04
screen shot 2017-01-19 at 17 16 52
screen shot 2017-01-19 at 17 17 09
screen shot 2017-01-19 at 17 17 26
this is the way im subscribing (its on main thread)
func getApp() { return providerCert.request(RestAPI.getApp) .filterSuccessfulStatusCodes() .mapJSON().subscribe(onNext: { result in print(result) }) }

@beretis We added this extension, and using it every time we use moya

// Fixed Moya scheduler issue
extension RxMoyaProvider {

    func requestBackground(target: Target) -> Observable<Response> {
        return request(target).observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
    }
}

like provider.requestBackground({{Target}}), so with .observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background)) we force the Observable chain to perform the api call on background

Thank you, for swift 3 compatibility if anyone is interested

extension RxMoyaProvider {

    func requestBackground(target: Target) -> Observable<Response> {
        return request(target).observeOn(SerialDispatchQueueScheduler(qos: DispatchQoS.background))
    }
}
Was this page helpful?
0 / 5 - 0 ratings