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

Incorporate feedback from #130 #138

Merged
merged 25 commits into from
Dec 3, 2017
Merged

Incorporate feedback from #130 #138

merged 25 commits into from
Dec 3, 2017

Conversation

ZacSweers
Copy link
Collaborator

@ZacSweers ZacSweers commented Nov 18, 2017

This PR incorporates feedback from #130, but it not currently complete yet. CC @akarnokd whenever you have time to review (no rush!)

Completed

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

Mentioned in the issue, opted to keep this to match semantics of RxJava's plugins. We also operate on a large monorepo structure internally where this actually becomes useful to help catch downstream usages.

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

Touched on in the issue, and after thinking on it I think I'm going to keep this as is until there's a new minor release or new API we want to target. Keeping the lower API makes it easier for consumers to adopt without imposing newer versions if they haven't been able to update yet.

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.

bd22bc2

AutoDisposingCompletableObserver.java#L30: missing space in forintrospection.

caf904a

AutoDisposingObserver.java#L30: missing tailing s from purpose?

79e6dbf

AutoDispose.java#L169: class could be final.
AutoDispose.java#L199: class could be final.
AutoDispose.java#L229: class could be final.

94212eb

AutoDisposingCompletableObserverImpl.java#L45: The DisposableMaybeObserver could be created and setOnce'd before the call to lifecycle.subscribe().

9bf1993

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).

cb432a8. I think I've mostly got this. 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)

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.

07f0f1e and corresponding one for LifecycleEventsObservable ec190c0

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.

I think this is covered as a part of 07f0f1e, but let me know if I've missed something

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.

b38d03f

TODO

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.

If I subscribe first - couldn't it synchronously execute its entire sequence before the lifecycle gets a chance then? Part of ths reason it's done ater the lifecycle check is such that the lifecycle can indicate that it's ended, and just immediately dispose after onSubscribe().

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.

Could you expand on this one a bit? I'm not sure I totally understood

AutoDisposingSubscriberImpl.java#L80: 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.

See comment below

Adds a convenience assertion and uses peek instead of poll for hasError
These were missing before and necessary to mark this observer as appropriately disposed after, though without calling dispose()
Saving dex methods
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)
@ZacSweers
Copy link
Collaborator Author

Could you expand on this one a bit? I haven't used flowable's APIs that much so I'm a little inexperienced with handling requesting or deferring it

I actually missed the link in your original comment. Going to research more with that and report back!

@ZacSweers
Copy link
Collaborator Author

Ok I think I've got deferred requesting set up in 500f337 after reading the docs. If that looks good, I'll bring those methods into the AutoDisposeSubscriptionHelper copy so it's not depending directly on internal classes

@ZacSweers
Copy link
Collaborator Author

Ok, I'm missing something about how to wire up deferred requesting. Let me know if you have any advice!

@akarnokd
Copy link
Contributor

Here is the wiki entry for deferred requesting.

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.

Could you expand on this one a bit? I'm not sure I totally understood

Does the lifecycle's onComplete get called while there is still a subscriber active? Consider this:

PublishSubject.create()
.to(AutoDispose.with(ScopeProvider.UNBOUND).forObservable())
.subscribe(new DisposableObserver<Object>() {
    PrintWriter fout;

    @Override public void onStart() {
        fout = new PrintWriter(new FileWriter("output.txt"));
    }

    @Override public void onNext(Object o) {
        fout.println(o);
    }

    @Override public void onError(Throwable t) {
        t.printStackTrace(fout);
        fout.close();
    }

    @Override public void onComplete() {
        fout.close();
    }
});

If the lifecycle ends, fout may left open.

@ZacSweers
Copy link
Collaborator Author

ZacSweers commented Nov 26, 2017

Yep that's correct behavior. Something may be considered "unbound" in the sense that they won't be disposed by the lifecycle ending. In the example case, it would be as if it was running without autodisposing. The intention for ScopeProvider.UNBOUND is to be a shortcut for upstream providers to pass down. Doing it exactly as your snippet does would be considered sort of wasteful, in the same way takeUntil(Observable.never()) might be.

For the deferred requesting, I tried implementing pretty much like the Operator example in the wiki, but I get an error I don't quite understand when I try to test with it:

java.lang.AssertionError: Not subscribed! (latch = 1, values = 0, errors = 0, completions = 0)

	at io.reactivex.observers.BaseTestConsumer.fail(BaseTestConsumer.java:162)
	at io.reactivex.subscribers.TestSubscriber.assertSubscribed(TestSubscriber.java:309)
	at com.uber.autodispose.AutoDisposeSubscriberTest.autoDispose_withProvider(AutoDisposeSubscriberTest.java:124)

Does anything stand out to you as off in my implementation?

@akarnokd
Copy link
Contributor

Could you point me to the implementation that is targeted by that test?

@akarnokd
Copy link
Contributor

You have to call delegate.onSubscribe(this) before L74.

@ZacSweers
Copy link
Collaborator Author

ZacSweers commented Nov 26, 2017

That unfortunately causes several other tests to fail with a double subscription error:

java.lang.AssertionError: Error(s) present: [java.lang.IllegalStateException: onSubscribe received multiple subscriptions: EmptySubscription] (latch = 1, values = 0, errors = 1, completions = 0)
	at io.reactivex.observers.BaseTestConsumer.fail(BaseTestConsumer.java:162)
	at io.reactivex.observers.BaseTestConsumer.assertNoErrors(BaseTestConsumer.java:247)
	at com.uber.autodispose.AutoDisposeSubscriberTest.autoDispose_withScopeProviderCompleted_shouldNotReportDoubleSubscriptions(AutoDisposeSubscriberTest.java:347)

java.lang.AssertionError: Error(s) present: [java.lang.IllegalStateException: onSubscribe received multiple subscriptions: EmptySubscription] (latch = 1, values = 0, errors = 1, completions = 0)
	at io.reactivex.observers.BaseTestConsumer.fail(BaseTestConsumer.java:162)
	at io.reactivex.observers.BaseTestConsumer.assertNoErrors(BaseTestConsumer.java:247)
	at com.uber.autodispose.AutoDisposeSubscriberTest.autoDispose_withProviderAndNoOpPlugin_afterEnding_shouldFailSilently(AutoDisposeSubscriberTest.java:247)

Specifically -

AutoDisposeSubscriberTest. autoDispose_withProviderAndNoOpPlugin_afterEnding_shouldFailSilently
AutoDisposeSubscriberTest. autoDispose_withProviderAndNoOpPlugin_withoutStarting_shouldFailSilently
AutoDisposeSubscriberTest. autoDispose_withProviderAndPlugin_withoutStarting_shouldFailWithExp
AutoDisposeSubscriberTest. autoDispose_withProvider_afterLifecycle_shouldFail
AutoDisposeSubscriberTest. autoDispose_withProvider_withoutStartingLifecycle_shouldFail
AutoDisposeSubscriberTest. autoDispose_withScopeProviderCompleted_shouldNotReportDoubleSubscriptions

@ZacSweers
Copy link
Collaborator Author

Also - calling delegate.onSubscribe() before the lifecycle kind of gets to the other remaining concern I had in the description:

If I subscribe first - couldn't it synchronously execute its entire sequence before the lifecycle gets a chance then? Part of the reason it's done after the lifecycle check is such that the lifecycle can indicate that it's ended, and just immediately dispose after onSubscribe().

Would be curious to know your thoughts

@akarnokd
Copy link
Contributor

You don't need to call callMainSubscribeIfNecessary as the delegate's onSubscribe will be called before the lifecycle is subscribed.

@ZacSweers
Copy link
Collaborator Author

D'oh, I totally missed that. Fixed in e7e1b87. With that, I think I've covered the points from your issue if you want to do a full review?

if (!isMainThread()) {
observer.onError(
new IllegalStateException("Lifecycles can only be bound to on the main thread!"));
return;
}
ArchLifecycleObserver archObserver =
new ArchLifecycleObserver(lifecycle, observer, eventsObservable);
observer.onSubscribe(archObserver);
lifecycle.addObserver(archObserver);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the Observer disposes immediately, this will still add the listener and keep a reference to it. You should check for isDisposed after this add and call removeObserver:

lifecycle.addObserver(archObserver);
if (archObserver.isDisposed()) {
    lifecycle.removeObserver();
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -46,8 +48,6 @@
// Emit the last event, like a behavior subject
observer.onNext(ViewLifecycleEvent.ATTACH);
}
Listener listener = new Listener(view, observer);
observer.onSubscribe(listener);
view.addOnAttachStateChangeListener(listener);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too, if disposed, the listener will remain added.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -166,7 +166,7 @@
return new LifecycleScopeProviderHandlerImpl(scope);
}

private static class MaybeScopeHandlerImpl implements ScopeHandler {
private static final class MaybeScopeHandlerImpl implements ScopeHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd avoid private in case you don't want to get accessor methods on Android.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in 959fe09 but planning to remove this class in the near future anyway now that as() is in RxJava :)

}
};
if (AutoDisposeEndConsumerHelper.setOnce(lifecycleDisposable, o, getClass())) {
lifecycle.subscribe(o);
if (AutoDisposeEndConsumerHelper.setOnce(mainDisposable, d, getClass())) {
delegate.onSubscribe(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callMainSubscribeIfNecessary may be a problem here too causing double onSubscribes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the only events that could cause this though would be if the lifecycle terminated, in which case mainDisposable would either be dispose()'d or be lazySet to DISPOSED (and thus avoid a double subscription error from being thrown) right? Or do you mean there's race condition potential if the lifecycle terminates in the middle of this code execution?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you can confine this entire onSubscribe to the main thread, it is possible this gets executed asynchronously while the main thread ends the lifecycle.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think I see what you mean. I can call delegate.onSubscribe early rather than try to be defensive against lifecycle ending immediately (which is what I keep being worried about, as it bit me in the past). We're already subscribed at this point so it's not like calling onSubscribe on the delegate early is going to change anything

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ba8bd87

delegate.onComplete();
}
}

@Override public void onError(Throwable e) {
if (!isDisposed()) {
lazyDispose();
AutoDisposableHelper.dispose(lifecycleDisposable);
mainDisposable.lazySet(AutoDisposableHelper.DISPOSED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swap these so the mainDisposable change gets flushed with the dispose() call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in just swap the order they're executed?

mainDisposable.lazySet(AutoDisposableHelper.DISPOSED);
AutoDisposableHelper.dispose(lifecycleDisposable);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming so after googling a bit - part of 9c3edf4

}
};
if (AutoDisposeEndConsumerHelper.setOnce(lifecycleDisposable, o, getClass())) {
lifecycle.subscribe(o);
if (AutoDisposeEndConsumerHelper.setOnce(mainDisposable, d, getClass())) {
delegate.onSubscribe(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callMainSubscribeIfNecessary may cause double onSubscribe here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of ba8bd87

@@ -95,21 +91,24 @@ void callMainSubscribeIfNecessary(Disposable d) {

@Override public void onSuccess(T value) {
if (!isDisposed()) {
lazyDispose();
AutoDisposableHelper.dispose(lifecycleDisposable);
mainDisposable.lazySet(AutoDisposableHelper.DISPOSED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swap these.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of 9c3edf4

}
};
if (AutoDisposeEndConsumerHelper.setOnce(lifecycleDisposable, o, getClass())) {
lifecycle.subscribe(o);
if (AutoDisposeEndConsumerHelper.setOnce(mainDisposable, d, getClass())) {
delegate.onSubscribe(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double onSubscribe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of ba8bd87

}
};
if (AutoDisposeEndConsumerHelper.setOnce(lifecycleDisposable, o, getClass())) {
lifecycle.subscribe(o);
if (AutoDisposeEndConsumerHelper.setOnce(mainDisposable, d, getClass())) {
delegate.onSubscribe(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double onSubscribe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of ba8bd87

@ZacSweers
Copy link
Collaborator Author

I think I've addressed the existing comments and a little extra. Thanks again for taking the time to review this, I'm learning a lot!

@ZacSweers
Copy link
Collaborator Author

@akarnokd let me know if there's anything else you see, otherwise I'll try to land this monday or tuesday

@akarnokd
Copy link
Contributor

akarnokd commented Dec 3, 2017

No further comments. 👍

@ZacSweers
Copy link
Collaborator Author

Awesome. Thank you so much for the review!

@ZacSweers ZacSweers merged commit 16a517f into master Dec 3, 2017
@ZacSweers ZacSweers deleted the z/feedback branch December 3, 2017 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants