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

Add takeUntil support in Single #3712

Merged
merged 2 commits into from
Feb 23, 2016
Merged

Conversation

ZacSweers
Copy link
Contributor

As discussed in #3708

This adds takeUntil(Observable) and takeUntil(Single) support in Single. It was mostly just adapting the logic from the existing OperatorTakeUntil and adjusting it for accepting a Single and sending a CancelattionException in the event of a submission from other prior to a terminal event in the source Single.

Any feedback is appreciated it, this is my first time contributing an implementation to this project. Particularly wondering if it's worth keeping both overloads or if the user should just coerce their other to one type or ther other. Also particularly looking for feedback on what information to include in the CancellationException.

@ZacSweers
Copy link
Contributor Author

Just noticed my IDE swapped the wildcard imports for explicit ones. Let me know if I should revert that.

@akarnokd
Copy link
Member

Yes please.


@Test
@SuppressWarnings("unchecked")
public void testTakeUntilSuccess() {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for test prefix here and in tests below

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please add Single and Observable to all test names appropriately?

Idk, but current tests names are not very obvious, would be easier to see what to expect from tests if they have names like takeUntil_should..When.. but it's personal thing, so feel free to ignore :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the test prefix: 322ae57

I wasn't sure what to do about the names. I figured since it was in SingleTest that I could assume any test without an explicit "Observable" mention was using "Single". Maybe it's not that obvious though. @akarnokd what do you think?

@artem-zinnatullin
Copy link
Contributor

Just style issues, otherwise LGTM 👍

@ZacSweers
Copy link
Contributor Author

CC @akarnokd @artem-zinnatullin

For the tests, most of them are adapted from OperatorTakeUntilTest. It looks like these tests have two different styles (one with TestObservable and the other with Subject). I reused both of them here, but would we maybe want to stick with one or the other? Especially with Single, I think some now overlap.

Also, if we stick with one style here, should we update the style in OperatorTakeUntilTest as well to match?

@akarnokd
Copy link
Member

I'm not too keen on how the tests are named or what test framework objects you use.

Please squash your commits.

@ZacSweers
Copy link
Contributor Author

Squashed

@akarnokd
Copy link
Member

👍

@ZacSweers
Copy link
Contributor Author

@zsxwing @stevegury any input? Need another collaborator review

Main points I'm wondering about:

  • Should we only support one overload? If we support more than one, should it support all three (including Completable?)
  • Should takeUntil test styles formalized, looks like there are two styles right now judging by OperatorTakeUntilTest: subject-based and TestObservable-based.

@stevegury
Copy link
Member

👍
Yes, I think it makes sense to had an override for Completable.
Regarding test styles, I don't think it's a big deal. The best thing to do slowly convert all the tests to the new convention (ideally in separated commits that don't change the behavior).

@zsxwing
Copy link
Member

zsxwing commented Feb 17, 2016

sending a CancelattionException in the event of a submission from other prior to a terminal event in the source Single.

This behavior is really confusing. Why not be same as Observable? Am I missing anything?

@ZacSweers
Copy link
Contributor Author

@akarnokd and I discussed it in #3708. Single currently emits a NoSuchElementException if the stream is terminated prior to any event emission. By signaling a CancellationException, we can at least signal to the subscriber if the error was due to takeUntil unsubscribing it or normal missing emissions.

@zsxwing
Copy link
Member

zsxwing commented Feb 17, 2016

@hzsweers Thanks for clarifying. However, I would expect Single.takeUntil returns something that contains 0 or 1 item. How about returning Observable instead? Then the semantics of Single.takeUntil is onNext{0, 1} (onCompleted | onError), which is consistent with Observable.takeUntil (onNext{0, N} (onCompleted | onError))

@akarnokd
Copy link
Member

The operators should stay in the same type as long as they can and there are operators that simply can't behave the same as their counterpart in other reactive types. If one wishes the Observable.takeUntil behavior, one can use toObservable().takeUntil() and get that behavior

@zsxwing
Copy link
Member

zsxwing commented Feb 17, 2016

The operators should stay in the same type as long as they can and there are operators that simply can't behave the same as their counterpart in other reactive types. If one wishes the Observable.takeUntil behavior, one can use toObservable().takeUntil() and get that behavior

If so, I vote for NoSuchElementException as CancellationException seems counterintuitive to me. CancellationException looks some codes cancel Single (e.g., I call unsubscribe in some place). But here the error is that Single.takeUntil returns something that is not Single. Hence, I would expect NoSuchElementException.

@ZacSweers
Copy link
Contributor Author

That's fair, but is there something we could do to at least indicate whether the source was just unsubscribed or actually misbehaved? I was of the impression that onError was primarily for serious, unexpected errors, whereas this seems like not unexpected or serious.

Consider the following

With NoSuchElementException:

PublishSubject<Integer> source = PublishSubject.create();
PublishSubject<Integer> until = PublishSubject.create();

source.take(1).toSingle()
        .takeUntil(until.take(1).toSingle())
        .subscribe(
                new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        System.out.println("Success");
                    }
                },
                new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        System.out.println("I don't know if it was due to unsubscribing or the source is misbehaving");
                    }
                });

until.onNext(1);

vs. with CancellationException

PublishSubject<Integer> source = PublishSubject.create();
PublishSubject<Integer> until = PublishSubject.create();

source.take(1).toSingle()
        .takeUntil(until.take(1).toSingle())
        .subscribe(
                new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        System.out.println("Success");
                    }
                },
                new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        if (throwable instanceof CancellationException) {
                            System.out.println("It was canceled.");
                        } else {
                            System.out.println("Source didn't emit.");
                        }
                    }
                });

until.onNext(1);

The example I gave in the issue is probably the best example for me. We use something similar to this for lifecycle binding in android. When the lifecycle ends, it might unsubscribe this in the middle. For a normal error, we might show a generic "an error occurred" message. In the event that it's just the lifecycle ending, we don't want to react that way, and rather likely just want to do nothing at all or clean up resources. I'm fine with not using CancellationException, but I think it would be useful to add a means of being able to differentiate between unsubscription reasons.

I thought about just specifying a message, but felt that @akarnokd's CancellationException suggestion made it more clear.

@ZacSweers
Copy link
Contributor Author

I'll add an overload for Completable as well, and remove the old test style in the new tests here for now. @stevegury just to be clear, the "new" style would be the Subject-based approach right?

@stevegury
Copy link
Member

@hzsweers I actually typed "it's a big deal" but I was willing to say "it's not a big deal" (I corrected my previous comment). I prefer the Subject based approach, but I am not strongly opinionated about that.
I still 👍 on the current implementation.

@zsxwing
Copy link
Member

zsxwing commented Feb 17, 2016

That's fair, but is there something we could do to at least indicate whether the source was just unsubscribed or actually misbehaved?

My point here is if Single.takeUntil(...) returns a Single that emits nothing, it should be misbehaved rather than unsubscribed.

@akarnokd
Copy link
Member

I think "CancellationException" is the clearer reaction here. Remember the problems around the Observable.single() and how it is a source of problem to find out exactly who didn't signal? Here, you know that if takeUntil is tripped and not some upstream machinery ends up being empty.

@zsxwing
Copy link
Member

zsxwing commented Feb 17, 2016

I think "CancellationException" is the clearer reaction here. Remember the problems around the Observable.single() and how it is a source of problem to find out exactly who didn't signal? Here, you know that if takeUntil is tripped and not some upstream machinery ends up being empty.

What if we want to add other operator that may return something doesn't signal? If it also emits CancellationException, then we still cannot find out exactly who didn't signal. So why not use NoSuchElementException to indicate all cases that didn't signal?

@ZacSweers
Copy link
Contributor Author

What about a subclass of NoSuchElementException? A CanceledNoSuchElementException, so to speak. Wouldn't break the downstream APIs but still allows for the subscriber to differentiate.

@zsxwing
Copy link
Member

zsxwing commented Feb 17, 2016

What about a subclass of NoSuchElementException? A CanceledNoSuchElementException, so to speak. Wouldn't break the downstream APIs but still allows for the subscriber to differentiate.

Maybe a more general question, should we add special exceptions for different operators, or we just use a general exception to indicate the same error?

@ZacSweers
Copy link
Contributor Author

I think people should be aware of the implications of operators they use. Would CompositeException possibly be precedent for this?

@zsxwing
Copy link
Member

zsxwing commented Feb 17, 2016

I think people should be aware of the implications of operators they use. Would CompositeException possibly be precedent for this?

CompositeException is fine and clear. It indicates there are multiple errors thrown. All classes in rx.exceptions are well defined, and it's very easy to connect them with the bad cases. I can just read the exception name and tell what my codes violate. But for CancellationException, it's hard to connect it with takeUntil, especially people use some library that calls takeUntil internally. In addition, IMO, if I see CancellationException, my first thought is Future.cancel is called somewhere.

If we can define clearly that when should throw CancellationException/CanceledNoSuchElementException(or whatever you propose), when should throw NoSuchElementException, I won't be against that. For now, the confusing thing for me is that if a Single doesn't signal, I may receive CancellationException or NoSuchElementException.

@ZacSweers
Copy link
Contributor Author

I'm fine with either. I think we could be clear in the documentation, and subclassing would still allow downstream subscribers to treat it as a NoSuchElementException. @akarnokd @stevegury thoughts?

@ZacSweers
Copy link
Contributor Author

Added takeUntil(Completable) support and standardized tests. Updated with a separate commit for easier reviewing, let me know if you want me to squash again.

ping @akarnokd @stevegury

@akarnokd
Copy link
Member

👍

1 similar comment
@stevegury
Copy link
Member

👍

stevegury added a commit that referenced this pull request Feb 23, 2016
@stevegury stevegury merged commit 3e6affd into ReactiveX:1.x Feb 23, 2016
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.

5 participants