Rxjs: .timeout doesn't cancel setTimeout

Created on 15 Nov 2016  路  20Comments  路  Source: ReactiveX/rxjs

RxJS version:
rxjs beta.12
Code to reproduce:

Expected behavior:
cancel the internal setTimeout when it's not needed anymore (after first response)

Actual behavior:
setTimeout still resolves which delays universal since zone.js tracks it
Additional information:

Angular Universal uses zone.js to keep track of async calls before rendering. There seems to be a problem with .timeout on an http call
here's a repro
https://github.com/gdi2290/universal-starter-rxjs-timeout-issue/blob/master/src/app/shared/api.service.ts#L23
npm run watch:dev
comment out that line
there are some console logs on the server that show the stable time taking 2000ms with the timeout rather than less than 1ms

All 20 comments

Please allow me quick question to understand this - so is this about cancelling scheduled timeout per each emit as soon as value emits?

@kwonoj yup

as I have barely no experience with zone.js I can't say for this really sure, so I'll leave this open and /cc @blesh and @jayphelps . Personally this sounds more of issue of zone.js though. If this is behavior of zone, it might possible other codebase in Rx which rely on scheduled action until next value emit might possibly being affected as well.

I think (personally) it's fair that if the destination observable is unsubscribed, that any pending setTimeout/setInterval should also be cancelled.

https://github.com/ReactiveX/rxjs/blob/e0cf882feab1ec46cd611434e6411bd867abcbf2/src/operator/timeout.ts#L75

I think in this scenario, it may be as simple as adding the Action to the destination subscriber.

ie...

this.scheduler.schedule(TimeoutSubscriber.dispatchTimeout, this.waitFor, { subscriber: this, index: currentIndex });

// becomes

this.destination.add(this.scheduler.schedule(TimeoutSubscriber.dispatchTimeout, this.waitFor, { subscriber: this, index: currentIndex }));

Makes me wonder how buffer / window behave.

@david-driscoll but I assume those will not solve this specific issue since issue is about cancelling timeout per-each-value-emit. am I understand correctly?

Let me reword it with another example just to be sure everyone understands the problem we ran into. Users are currently following this pattern as a workaround for .timeout which isn't correct (commented).

Observable.race(
  this.http.get(url),
  Observable.of(null).delay(2000) // I would prefer to return immediately if the other observables in the race are done/errored/complete
)
.subscribe(data => this.data = data)

The idea is to cancel the http observable if it exceed the timeout limit otherwise if the http observable first then cancel the timeout (in this case the internal setTimeout).

For zone.js etc in Angular Universal, we are using zone.js to track when the application is stable before rendering the Application. The problem here happens when the setTimeout is still resolving even if the http call is done which prevents rendering

I would think that if a timeout subscriber has been cancelled / completed that any pending timeouts should be cancelled. So that we clean up after ourselves.

What zone.js does is simply wraps anything async in the browser, timeouts, promises, etc. Using the zone.js api it's then possible to see if there are any scheduled tasks at a given time.

For this problem, there is always an outstanding task when timeout is subscribed to, which never gets cancelled, so zone.js will see a pending change for 2000ms regardless of the actual completion of the event.

My code changes are incorrect, we would need to keep track of the base subscription, and unsubscribe in _next, _error and _complete. Additionally hasCompleted would no longer be needed as dispatchTimeout would only be called the timeout actually happened.

I'll leave it out for @blesh and @jayphelps to chime in on additionally.

@david-driscoll yup you said it perfectly 馃憤

A bit pedantic, but just for clarity, we actually use setInterval to schedule async things, even if they only occur once.

Edit: the following is wrong. Schedulers do support cancellation.

We don't currently provide any mechanism to cancel scheduled work. So every operator which schedules, that work will be invoked regardless of whether or not anyone is subscribing anymore. That work then double checks whether or not it should proceed, e.g. checking source.hasCompleted. Presumably, there are other operator usages that will prevent ng2 from flushing as soon as it could.

Certainly an argument could be made that we should provide a cancellation mechanism. A quick look at v4 suggests it did. No guarantees, but this might make our operator code cleaner and less error prone as well, since the work callback wouldn't need to check whether or not the source has completed I think.

Cc/ @trxcllnt

@jayphelps you are correct, it did, which should be the default behavior to always tear down upon cancellation/completion and not just removing the item from the queue.

@jayphelps We don't currently provide any mechanism to cancel scheduled work. So every operator which schedules, that work will be invoked regardless of whether or not anyone is subscribing anymore.

This is not the case. Canceling an AsyncAction before the due time _will always_ clear the scheduled interval, full stop. Like @david-driscoll has pointed out, the timeout operator needs to clean up after itself.

@trxcllnt OH. Can you show where this is done? I only see a single clearInterval and do not see how it could be hit for anything but a reschedule on a different time.

@jayphelps here's the line that does it. To see it in action, drop this into esnextb.in:

import { Scheduler } from 'rxjs';

const subscription = Scheduler.async.schedule(function() {
  console.log('scheduled action executed');
}, 2000);

setTimeout(() => {
  console.log('canceling scheduled action');
  debugger
  subscription.unsubscribe();
}, 1000);

Since all scheduler actions extend the AsyncAction, this behavior is consistent across the schedulers.

@trxcllnt 馃憤 ahhh okay. Missed that. My brain couldn't see past the fact that it's called recycle 馃槣

@jayphelps the schedulers were a particular challenge. Attempting to take advantage of the more precise interval execution via setInterval vs. serial setTimeouts introduced a number of edge cases the schedulers have never encountered before. Additionally, we'd like to maximize code reuse, so AsyncAction needs an API that can be minimally extended to support asap, animationFrame, and virtual time/test scheduling. And last but not least, we need it to be speedy and lightweight.

I'll be the first one to admit the scheduler code is clever (in the :/ way), but it was forged through the flames of rigorous speed and memory tests, and I'm resigned to the idea that sometimes the code you need isn't the code you want. That said, I'm 100% on board for contributions and collaborations that improve quality, speed, etc. and happy to pair or answer questions for anybody who is interested in digging in.

@jayphelps ...it's called recycle

Heh, I totally understand. Naming is hard. My thought process was more, sometimes recycling means you reuse the object, other times you dispose of it. ;j

Oh, not to mention async (or asap/queue/animationFrame/virtual) actions can all reschedule themselves _with different due times_ per iteration of the work. One of the biggest improvements (imo) to the scheduling architecture is that repeatedly scheduled work (for example, on an interval) only allocates a single Subscription. Obviously doing this and still supporting all the behaviors Rx has always supported is, as evident by this discussion, complicated.

I met the same issue when trying to set global timeout for http request

ping @blesh @jayphelps

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

OliverJAsh picture OliverJAsh  路  3Comments

cartant picture cartant  路  3Comments

marcusradell picture marcusradell  路  4Comments

giovannicandido picture giovannicandido  路  4Comments

dooreelko picture dooreelko  路  3Comments