lgierth / promise.rb

Promises/A+ for Ruby

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

#sync breaks when mixing Promise classes

haileys opened this issue · comments

I'm running into a bug in promise.rb where the sync method breaks when mixing various Promise classes together.

Here's some sample code demonstrating the problem:

require "promise"

class MyPromise < ::Promise
  def wait
    fulfill(456)
  end
end

p1 = Promise.resolve(123)

p2a = MyPromise.new
p2a.sync

p2b = MyPromise.new

p3a = p1.then { |a| p2a.then { |b| a + b } }
p3b = p1.then { |a| p2b.then { |b| a + b } }

puts p3a.sync
puts p3b.sync

What's happening is that when then is called on p1 (which is already resolved), promise.rb internally creates a new promise of the same type as p1 and then immediately executes the then block. Even though the then block is called immediately and returns a new MyPromise instance, the internal state of the promise returned by the then block is copied into the newly created Promise instance rather than the MyPromise instance being returned directly.

If p2a is also already resolved, this is not a problem and calling p3a.sync returns the right thing.

If p2b is not already resolved when p3b.sync is called, the sync call tries to call wait on an instance of Promise, rather than an instance of MyPromise. This raises a NoMethodError.

The output I'd expect the code sample above to produce is:

579
579

The actual output is:

579
lib/promise.rb:64:in `sync': undefined local variable or method `wait' for #<Promise:0x007fb8db51e028 @state=:pending, @callbacks=[]> (NameError)
    from x.rb:20:in `<main>'

Yes, that is the problem with mixing promise classes. This can be avoided by using p1 = MyPromise.resolve(123) so that you don't mix promise classes.

However, I don't think this is a bug.

To start with, you have an incorrect assumption that the block passed to then is executed immediately when the receiver Promise instance has already been resolved. The promise spec expects the onFulfilled or onRejected to be deferred. Promise#defer can be overridden to implement this behaviour and conform to the spec as documented in the README's usage section. This prevents the Promise#then method from being able to determine the class of a promise that may be returned from the block.

The other problem with what you are suggesting is that it would introduce racey behaviour in the type of value that is returned from Promise#then in code where the Promise instance could be resolved by another fiber or thread that is running in parallel. I would much prefer to have Promise#then deterministically return a result based on the type of the receiver so that it is easier to catch errors from misuse during testing.