Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Review of RxJava 2 usage in the project #130

Closed
akarnokd opened this issue Nov 10, 2017 · 8 comments
Closed

Review of RxJava 2 usage in the project #130

akarnokd opened this issue Nov 10, 2017 · 8 comments

Comments

@akarnokd
Copy link
Contributor

Hi. I was asked to review the library's RxJava 2 usage for potential logical or concurrency issues.

Review @ commit: 0147484b

Main issues:

  • AutoDisposingCompletableObserverImpl.java#L45: The DisposableMaybeObserver could be created and setOnce'd before the call to lifecycle.subscribe().
  • AutoDisposingCompletableObserverImpl.java#L52: The mainDisposable could be already set and the delegate currently executing one of its onXXX methods which violates the contract unless it is guaranteed both the lifecycle signal and the onXXX calls happen on the same thread (i.e., forced observeOn(AndroidSchedulers.mainThread())). Possible solution: always call delegate.onSubscribe(this) first, mutually exclude the onXXX signals via an AtomicBoolean once.
  • AutoDisposingCompletableObserverImpl.java#L58: May not be a good idea to leave the delegate non-terminated in this case, if the lifecycle actually can get onComplete.
  • AutoDisposingCompletableObserverImpl.java#L73: synchronized is pointless here.
  • AutoDisposingCompletableObserverImpl.java#L80: synchronized is pointless here. If the purpose was to mutually exclude with #L73 then it is not worth it. If lazySet wins then dispose(ref) will do nothing, if they race, both cases could win probabilistically.
  • AutoDisposingMaybeObserverImpl.java: Same problems as with the CompletableObserver implementation.
  • AutoDisposingObserverImpl.java: Same problems as with the CompletableObserver implementation. Here one would need a HalfSerializer to mutually exclude the onXXX events (as there could be multiple onNext calls).
  • AutoDisposingSingleObserverImpl.java: Same problems as with the CompletableObserver implementation.
  • AutoDisposingSubscriberImpl.java: Same problems as with the CompletableObserver implementation and then some.
  • AutoDisposingSubscriberImpl.java#L80: There is no onStart provided by the API and the delegate.onSubscribe is called after mainSubscription is definitely set: the null check should always pass and thus unnecessary. Note though that if one does the setOnce(mainSubscription) before the lifecycle is attached, one may need to use deferred requesting to not trigger the upstream emission while the code is setting up other things in onSubscribe.
  • ViewAttachEventsObservable.java#L41 If the call is not on the main thread, onSubscribe is not called which can lead to a NullPointerException in the dowstream. The no-op Disposable should be sent to the Observer first.
  • ViewAttachEventsObservable.java#L51: The Observer may cancel immediately and since the listener is not registered with the view yet, it won't get removed but then added unconditionally. Assuming the events also come from the main thread, one could add the listener first, and call onSubscribe next. If adding the listener triggers an event emission immediately, keep the current order and after adding the listener, check if the listener has been disposed in the meantime and remove the listener.
  • LifecycleEventsObservable.java#L78: Same problems as with the ViewAttachEventsObservable.

Minor issues:

@ZacSweers
Copy link
Collaborator

Thank you so much David! I'll get on these today. Would you mind if I added you as a reviewer on the subsequent PR(s)?

@akarnokd
Copy link
Contributor Author

Sure.

@ZacSweers
Copy link
Collaborator

ZacSweers commented Nov 11, 2017

Working on a branch and had a few questions/comments.

The DisposableMaybeObserver could be created and setOnce'd before the call to lifecycle.subscribe()

Is this due to using subscribeWith()? If so, would the safer solution be to implement the full observer + disposable setting manually in onSubscribe()?

AutoDisposePlugins.java#L36: Such lockdown support may not be desirable on Android. The same feature was aimed at server/J2EE container use of RxJava.

True, but this was built to be a Java library first with android extensions :). Mostly added to keep the semantics familiar to RxJava.

dependencies.gradle#L65: Using a pretty old RxJava 2 version.

Good point. I tend to prefer making extension libraries depend on lower versions of libraries to avoid imposing burden on consumers that maybe haven't been able to update yet (we hit this often). I suppose it could also become a liability if new changes caused unknown behavior changes here. I'll think on this more.

AutoDisposingCompletableObserverImpl.java#L58: May not be a good idea to leave the delegate non-terminated in this case, if the lifecycle actually can get onComplete.

I should elaborate that comment in source a bit. The behavior is such that if the scope "completes", then the binding is considered "unbound" and free to execute to termination. The idea is that scopes might sometimes be required for an API, but consumers of that API may want to just indicate that they can be unbound. They may dynamically resolve to be unbound under certain conditions at runtime too, and I wanted to support making that explicit. Otherwise scopes could just be defined as Single. Does that get to what you were raising above? If so - what do you think of that approach?

@akarnokd
Copy link
Contributor Author

Is this due to using subscribeWith()? If so, would the safer solution be to implement the full observer + disposable setting manually in onSubscribe()?

The problem is that this subscription happens before the associates setOnce even runs, thus the once-ness is enforced a bit late. I'd create the lifecycle observer type first, call setOnce and then lifecycle.subscribe() after.

If so - what do you think of that approach?

Risky. It would leave the consumer that is interested in handling onComplete without events, which could leak resources. This was a problem in RxLifecycle if I remember correctly.

@ZacSweers
Copy link
Collaborator

ZacSweers commented Nov 12, 2017

Could you expand on this bit some? I'm not sure I follow

consumer that is interested in handling onComplete without events

Particularly, the terminal emissions of the scope emissions are not visible outside of the autodisposing observer implementations, so I'm not sure which consumer would be interested in handling it. If the lifecycle onCompletes, the delegate is basically free to execute unbounded, and will never be manually disposed by AutoDispose.

@akarnokd
Copy link
Contributor Author

I'm sorry I have to postpone further explanations till next week as I have an important submission deadline coming up.

@ZacSweers
Copy link
Collaborator

No problem at all!

ZacSweers added a commit that referenced this issue Dec 3, 2017
* Remove unnecessary synchronized blocks

* Fix introspection typo

* Fix introspection typo in observer

* Make AutoDispose ScopeHandlers final

* Extract DisposableMaybeObserver and setOnce first

* Opportunistic tweak of RxErrorsRule

Adds a convenience assertion and uses peek instead of poll for hasError

* Lazyset disposables in scope callbacks to prevent double subscriptions

These were missing before and necessary to mark this observer as appropriately disposed after, though without calling dispose()

* Inline lazyDispose() methods

Saving dex methods

* Wire up HalfSerializer to observer and subscribers

I'm not 100% I've done this right, but leveraging some existing examples in RxJava as my guide. I've copied in the relevant internal rxjava APIs (since they're internal) and also adjusted the onNext methods to return true or false indicating whether or not they terminated the underlying observer. Will seek further feedback during the PR. Main thing I'm not sure of is if it does terminate, do we need to dispose the lifecycle or does this mean that onError or onComplete have been hit in the main observer already (and doing so is a double dispose condition)

* Fix ViewAttachEventsObservable not observing observer contract

* Fix LifecycleEventsObservable not observing observer contract

* Inline lazyDispose() impl

* Remove unnecessary nullcheck in request()

* Suppress nullaway false positive

* Initialize atomic throwables inline

* Use deferred requesting

* Call delegate.onSubscribe() first, remove callSubscribeIfNecessary

* Check if observer is disposed in lifecycle events

* Check if listener is disposed in viewattacheventsobservable

* Remove private to avoid synthetic accessor

* Use non-rx-internal subscriptionhelper APIs

* Opportunistic fix missing synchronized in overridden method

* Remove callSubscribeIfNecessary, avoid duplicate subscribes

* Switch orders of lazySet + dispose to flush

* Optimize lifecycle error propagation a bit

We were disposing the lifecycle a second time after, and unnecessarily disposing the main disposable twice too
@ZacSweers
Copy link
Collaborator

Done in #138

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants