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

Cancel scheduled timeout work, if no longer needed #2135

Merged
merged 37 commits into from
Mar 27, 2017

Conversation

jayphelps
Copy link
Member

@jayphelps jayphelps commented Nov 16, 2016

Fixes #2134

This ties the action subscription returned from a call to schedule() to the lifespan of the subscriber, so it handles error/complete. On reschedules it more explicitly cancels, if there's an existing.


I think this fixes it, but to be honest, I can't be sure. It's not immediately clear how to test it. At first glance, it doesn't appear we're testing this possible bug in the other operators which could have similar problems. e.g. debounceTime. So other operators could be hiding the fact that they don't clean up properly, because of guards in their work callback, or the subscriptions that protect against values continuing after error/complete.

I tried peeking into the internals of TestScheduler to see if maybe I could grab a reference to the VirtualAction and check action.pending, but because it's entirely virtual, the action comes and goes only inside a testScheduler.flush().

Perhaps @trxcllnt aka Mr. Scheduler can halp?

@mattpodwysocki
Copy link
Collaborator

@jayphelps perhaps there's a test we can write for this? I don't see anything here to that regard but should be.

@jayphelps
Copy link
Member Author

jayphelps commented Nov 16, 2016

@mattpodwysocki see my above post 😆

@mattpodwysocki
Copy link
Collaborator

@jayphelps * sigh * reading is fundamental...

@@ -45,6 +46,8 @@ class TimeoutOperator<T> implements Operator<T, T> {
class TimeoutSubscriber<T> extends Subscriber<T> {
private index: number = 0;
private _previousIndex: number = 0;
private action: Subscription = null;

get previousIndex(): number {
Copy link
Member

Choose a reason for hiding this comment

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

If we're tracking the scheduled action subscription, do we still need previousIndex etc.? Same question for hasCompleted.

Copy link
Member Author

Choose a reason for hiding this comment

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

not sure, but hasCompleted indeed seems extraneous. I couldn't figure out why there is an index (aka count) check anyway? Why would it ever not be equal?

const currentIndex = this.index;
const timeoutState = { subscriber: this, index: currentIndex };

this.cancelTimeout();
Copy link
Member

@trxcllnt trxcllnt Nov 16, 2016

Choose a reason for hiding this comment

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

So here it seems like we can do this:

const { action } = this;
if (!action) {
  this.add(this.action = this.scheduler.schedule(...));
} else {
  action.schedule(this.waitFor, timeoutState);
}

Instead of scheduling an action, throwing it away, and scheduling again, we can recycle the Action and reschedule it for a new due time. This should be more efficient, and allow us to remove more code from this file.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 I'm on board with reusing the action.

and allow us to remove more code from this file.

What other code could be removed?

@trxcllnt
Copy link
Member

trxcllnt commented Nov 16, 2016

@jayphelps we can use lift! If the changes in my review pan out, something like this should do the trick:

it('should unsubscribe from the scheduled timeout action when timeout is unsubscribed early', () => {
  const e1 =   hot('--a--b--c---d--e--|');
  const e1subs =   '^         !        ';
  const expected = '--a--b--c--        ';
  const unsub =    '          !        ';

  const result = e1
    .lift(function(source) {
      const timeoutSubscriber = this;
      const { action } = timeoutSubscriber; // get a ref to the action here,
      timeoutSubscriber.add(() => {         // because it'll be null by the
        if (!action.canceled) {             // time we get into this function.
          throw new Error('TimeoutSubscriber scheduled action wasn\'t canceled');
        }
      });
      return source._subscribe(timeoutSubscriber);
    })
    .timeout(50, null, rxTestScheduler);

  expectObservable(result, unsub).toBe(expected);
  expectSubscriptions(e1.subscriptions).toBe(e1subs);
});

@coveralls
Copy link

coveralls commented Nov 16, 2016

Coverage Status

Coverage decreased (-0.008%) to 97.677% when pulling d34cb57 on jayphelps:timeout into e0cf882 on ReactiveX:master.

@trxcllnt
Copy link
Member

trxcllnt commented Nov 16, 2016

@jayphelps I submitted a PR to your timeout branch.

trxcllnt added a commit to trxcllnt/rxjs that referenced this pull request Nov 16, 2016
…ed timeout actions.

The timeout and timeoutWith operators should dispose their scheduled timeout actions on

unsubscription. Also, given the new scheduling architecture, they can recycle their scheduled

actions so just one action is allocated per subscription.

ReactiveX#2134 ReactiveX#2135
trxcllnt added a commit to trxcllnt/rxjs that referenced this pull request Nov 20, 2016
…ed timeout actions.

The timeout and timeoutWith operators should dispose their scheduled timeout actions on

unsubscription. Also, given the new scheduling architecture, they can recycle their scheduled

actions so just one action is allocated per subscription.

ReactiveX#2134 ReactiveX#2135
jayphelps pushed a commit to jayphelps/rxjs that referenced this pull request Nov 20, 2016
…ed timeout actions.

The timeout and timeoutWith operators should dispose their scheduled timeout actions on

unsubscription. Also, given the new scheduling architecture, they can recycle their scheduled

actions so just one action is allocated per subscription.

ReactiveX#2134 ReactiveX#2135
jayphelps pushed a commit to jayphelps/rxjs that referenced this pull request Nov 20, 2016
…ed timeout actions.

The timeout and timeoutWith operators should dispose their scheduled timeout actions on

unsubscription. Also, given the new scheduling architecture, they can recycle their scheduled

actions so just one action is allocated per subscription.

ReactiveX#2134 ReactiveX#2135
@trxcllnt
Copy link
Member

@jayphelps I fixed the lint, rebased, tried and failed to push straight to this PR ref (GH doesn't like that), so I re-PR'd to your timeout branch.

jayphelps and others added 5 commits November 29, 2016 10:04
…heir scheduled work.

VirtualActions are immutable so they can be inspected by the TestScheduler. In order to mirror

rescheduled stateful Actions, rescheduled VirtualActions shouldn't execute if they've been

rescheduled before execution.
…ed timeout actions.

The timeout and timeoutWith operators should dispose their scheduled timeout actions on

unsubscription. Also, given the new scheduling architecture, they can recycle their scheduled

actions so just one action is allocated per subscription.
@coveralls
Copy link

coveralls commented Nov 29, 2016

Coverage Status

Coverage decreased (-0.008%) to 97.678% when pulling 0de6ef5 on jayphelps:timeout into f51b8f9 on ReactiveX:master.

@@ -89,7 +89,7 @@
"prepublish": "shx rm -rf ./typings && typings install && npm run build_all",
"publish_docs": "./publish_docs.sh",
"test_mocha": "mocha --opts spec/support/default.opts spec-js",
"debug_mocha": "node-debug _mocha --opts spec/support/debug.opts spec-js",
"debug_mocha": "node --inspect --debug-brk ./node_modules/.bin/_mocha --opts spec/support/debug.opts spec-js",
Copy link
Member

Choose a reason for hiding this comment

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

This change seems unrelated although I agree with it.

Copy link
Member Author

Choose a reason for hiding this comment

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

@trxcllnt I believe this is you, cool for me to remove?

Copy link
Member

Choose a reason for hiding this comment

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

as long as it's in a follow up PR that's fine with me. I didn't want to clutter up the PR list with internal config changes that only affect the devs working on the project, but I leave it up to your discretion

const unsub = ' ! ';

const result = e1
.lift(function(source) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this test is very straightforward. Perhaps it could be refactored into using the let operator, and returning a more basic observable

Copy link
Member Author

Choose a reason for hiding this comment

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

can we pair on this? I couldn't figure out how to test this at all, so @trxcllnt came up with this solution and it's still not clear to me how we could do it better. 💃

Copy link
Member

Choose a reason for hiding this comment

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

@Blesh let gives you a handle on the source Observable, but lift gives you a handle to each subscriber. In this case we want to know whether the subscriber did something (canceled its internal scheduled action subscription), so we use lift. textbook use-case for lift.

Copy link
Member

Choose a reason for hiding this comment

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

@trxcllnt ugh.. this is a case where we're abusing the fact that Operator has a call method, and we're therefor passing a function to lift here? This is really convoluted, it took me a while to figure that out. If anything this test reminded me why Operator probably shouldn't have a call method.

I think we need to find a different way to test this.

Copy link
Member

Choose a reason for hiding this comment

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

@jayphelps I think the way to test this, and other things like it, would be to add functionality to the TestScheduler so that it tracks the Actions it's created and those Actions can be examined to see at what tick they were cancelled. For now, we can leave these tests in here, but they'll need comments explaining what they're doing and why it works.

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically we need comments explaining why lift is magically accepting a function here and not an Operator instance.

We could simply pass an object with a call method.

.lift({
  call(source) { ... }
})

Copy link
Member

@trxcllnt trxcllnt Dec 12, 2016

Choose a reason for hiding this comment

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

@Blesh this is exactly why Operator has a call method. In an ideal world an operator would just be a function, but because JS sucks, we've had to make them classes. See RxJava, where Operators are functions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ben is OK with this for now, but wants some inline comments about why we're doing

const unsub = ' ! ';

const result = e1
.lift(function(source) {
Copy link
Member

Choose a reason for hiding this comment

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

Same as above

Copy link
Member

@benlesh benlesh left a comment

Choose a reason for hiding this comment

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

Basically we need comments explaining why lift is magically accepting a function here and not an Operator instance.

const unsub = ' ! ';

const result = e1
.lift(function(source) {
Copy link
Member

Choose a reason for hiding this comment

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

@jayphelps I think the way to test this, and other things like it, would be to add functionality to the TestScheduler so that it tracks the Actions it's created and those Actions can be examined to see at what tick they were cancelled. For now, we can leave these tests in here, but they'll need comments explaining what they're doing and why it works.

@trxcllnt
Copy link
Member

trxcllnt commented Dec 12, 2016

@Blesh alternatively, we should be able to define Operator as a hybrid type (so it's both an Object with a call method, and a Function with thisArg: Subscriber<T>), but my TypeScript-foo was not strong enough to defeat the compiler. maybe @david-driscoll or @kwonoj could help?

@trxcllnt
Copy link
Member

@Blesh what are the blockers to merging this? @jayphelps if possible, could you allow adding commits to this PR from other collaborators?

@jayphelps
Copy link
Member Author

@trxcllnt the edit bit is already enabled:

image

We discussed this PR today, minor changes needed: https://github.com/ReactiveX/rxjs-core-notes/blob/master/2016-12/december-20.md#review-prsissues

Needs comments on why we're using lift this way, also remove npm script changes.

I won't be able to get to it for at least a couple days, but others with edit bit are welcome to modify.

mpodlasin and others added 12 commits February 15, 2017 15:42
Return Observable when merge is called with single lower case observable,
so that merge would always return Observable instance.
Add binaryType to config object, so that it is possible
to set that parameter on underlying socket before any
data emits happen.

Closes ReactiveX#2353
Add type signature for using forkJoin with single observable as first parameter
and selector function as second parameter, so that typings would not prevent
usage which is permitted and properly handled by operator.

Closes ReactiveX#2347
…guments

Emit undefined insteady of empty array by resulting Observable,
when callback function is called without success parameters.

Closes ReactiveX#2254
In resulting Observable emit undefined when callback is called without
parameters, instead of emitting empty array.
The max line length is set to 150 in 'tslint.json'. In specific regions, it is
desirable to allow longer lines, so these regions should be wrapped in comments
like the following:

```js
// Max line length enforced here.

/* tslint:disable:max-line-length */
// Max line length NOT enforced here.
/* tslint:enable:max-line-length */   <-- CORRECT

// Max line length enforced here.
```

In many cases, the re-enabling comment incorrectly included `disable` instead of
`enable` (as shown below), which essentially keeps the `max-line-length`
**disabled** for the rest of the file:

```js
// Max line length enforced here.

/* tslint:disable:max-line-length */
// Max line length NOT enforced here.
/* tslint:disable:max-line-length */   <-- INCORRECT

// Max line length NOT enforced here.
```

This commit fixes these comments, so the `max-line-length` rule is properly
enforced in regions that don't need longer lines.
Adds new parameter in windowTime operator to control how much values given
window can emit.

Closes ReactiveX#1301
…descriptions

Add ObservableInput and SubscribableOrPromise interface descriptions,
as well as link these interfaces in type descriptions of operators,
so that users always know what kind of parameters they can pass to
used methods.
@kwonoj
Copy link
Member

kwonoj commented Feb 15, 2017

I'm going to lower commit message checker to warning so ignore failure for now please.

@benlesh
Copy link
Member

benlesh commented Feb 16, 2017

LOL... why are there so many commits in this PR now?

@trxcllnt
Copy link
Member

@Blesh lint/spec typings fixes and merge commits/conflicts le sigh

@kwonoj
Copy link
Member

kwonoj commented Feb 16, 2017

@trxcllnt sorry for inconvenience but rebase to master once again will resolve commit message checker failure (though you'll see warning still).

@benlesh
Copy link
Member

benlesh commented Feb 21, 2017

Ugh. I'm not sure what to do with this PR. It's older that dirt. It's a solid fix, but there's something crazy going on with the commits. Can we resubmit this one and/or clean it up?

@jayphelps
Copy link
Member Author

jayphelps commented Feb 21, 2017

hmm I'm not really following all of @trxcllnt's additions. If he's not available to fix, we can abandon this PR and I can try to fix the underlying problem again from a fresh start?

@trxcllnt
Copy link
Member

trxcllnt commented Feb 21, 2017

@jayphelps @Blesh they're just merge commits from master to resolve the merge conflicts. the CI was being really restrictive against merge commits, but @kwonoj relaxed that requirement in master, so I've done one more follow-up merge to hopefully make travis pass.

@coveralls
Copy link

coveralls commented Feb 21, 2017

Coverage Status

Coverage decreased (-0.008%) to 97.681% when pulling 9e16f48 on jayphelps:timeout into 6ce4773 on ReactiveX:master.

@lock
Copy link

lock bot commented Jun 6, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Jun 6, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

.timeout doesn't cancel setTimeout