Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Infinite loop interaction between q.js and when.js #106

Closed
briancavalier opened this Issue · 11 comments

4 participants

@briancavalier

I ran into this while using volo, which uses q, with when.js's when.map() to "volo add" (fetch from github and install) a bunch of AMD dependencies. I narrowed it down to an infinite loop that seems to be cause by an interaction between q's when() and/or promiseSend implementation and when.js's internal _reduce(), but I haven't been able to nail it down further than that.

Here's a fairly minimal test case that reproduces the problem using node v0.6.14 on OS X 10.8--not sure yet if it happens in other environments, but given that it seems to be related to when.js and q level code, I'm pretty sure it would. Interestingly, using when.js's delay() instead of q's seems to work ok.

Maybe we can put our heads together and figure out what's going on here. Thanks!

@briancavalier

I noticed that master is ahead of what's in npm (specifically that the double-when message in q.when() was removed), so I tried npm installing directly from the github url, and retried the test. I still get the infinite loop and CPU/mem spike.

I haven't dug any deeper yet, but I may this weekend. Any insight you guys have would be much appreciated.

@domenic
Collaborator

No real ideas off the top of my head, but FWIW this is definitely on my weekend to-do list.

@briancavalier

Thanks, Domenic.

@briancavalier

I still don't know what's going on, but I was able to trim down the test case. Now it shows the 4 possible combinations of .then() chaining using when.resolve and q.resolve. It seems to be some sort of weird interaction between the two since q + q works, and when + when works, but not when + q or q + when.

@briancavalier

I think I've narrowed it down a bit further. At first, I thought the nextTick() in q.when around 906 was unnecessary ... interestingly, commenting it out avoids the loop. But, after running Q's unit tests and looking more at the code, I see why it's there.

So, I tried a different angle. Here's another test case where it seems like using q.when() and deferred.promise.promiseSend() should produce equivalent behavior, but they done. I don't know if that assumption is valid, though. I tried some other combinations (using when.js's defer or when(), etc. etc.), but the loop seems to happen when I use q.when().

@domenic, have you found any clues?

@briancavalier

Oops, forgot the test case link, added above as well

@kriskowal
Owner

@briancavalier I’ve reproduced and came to the same conclusion. There is a cycle where Q and When are taking turns coercing eachother’s promises with their resolve methods. As part of this process, they both call .then on the other.

when can break the cycle by attempting to normalize the promise to a fulfilled value with promise.valueOf(). I’ll send a patch. I imagine Q could do something similar, but I don’t see an implementation of valueOf or nearer.

@domenic
Collaborator
@kriskowal
Owner

@domenic I suspect so. I’m favoring valueOf since the default implementation is harmless.

@kriskowal kriskowal closed this issue from a commit
@kriskowal Break coercion cycles with cooperating promises
Cooperating promise libraries must implement `valueOf` to extract the
fulfillment value from a fulfilled promise (or return the selfsame
promise for an unresolved or rejected promise).  This breaks cycles in
coercion, where one library uses `resolve` to coerce a foreign promise
to a local promise, which would in turn call `then` on the foreign
promise, which would in turn attempt to coerce the promise with its own
`resolve`, which would then in turn call `then` on our coerced promise,
ad nauseum.

Fixes #106.
facbb77
@kriskowal kriskowal closed this in facbb77
@briancavalier

Thanks, Kris, I really appreciate it. I'll try out the patch tonight with the volo use case where I first encountered the problem. If all goes well, I'll do a new release tomorrow.

I've actually resisted implementing valueOf (or nearer) because I felt it encourages people, especially those new to promises, to poll instead of observing with when/then. This might be a reason to implement it.

Interestingly, I haven't run into this problem when dealing with other promise impls (I've used when pretty extensively with Dojo Deferred pre-1.8 and, gasp, $.Deferred, among a few others). Those implementations don't coerce/assimilate (Dojo 1.8 does), though, which is probably the reason it all works out.

So, it seems like Promises/A can interoperate without valueOf, but perhaps only if there's at least one implementation in the mix that doesn't assimilate in order to break the cycle?

I'll certainly give more thought to adding valueOf to when.js.

@ForbesLindesay

If you force non-valueOf methods to resolve in the next turn of the event loop that can be very expensive when promises are resolved synchronously. I like to build as much as possible of my libraries to be OK with being given promises, so often they're given resolved promises, or plain old values.

@dfilatov dfilatov referenced this issue from a commit
Commit has since been removed from the repository and is no longer available.
@campadrenalin campadrenalin referenced this issue from a commit in DJDNS/when.js
@kriskowal Fix infinite coercion with Q
Some promises, particularly Q promises, provide a valueOf method that
attempts to synchronously return the fulfilled value of the promise, or
returns the unresolved promise itself.  Attempting to break a fulfillment
value out of a promise appears to be necessary to break cycles between
Q and When attempting to coerce each-other's promises in an infinite loop.
For promises that do not implement "valueOf", the Object#valueOf is harmless.
See: kriskowal/q#106
fd3f4df
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.