Promises
Squeak includes an implementation of promises, and this library uses them for RPC between actors.
Promises are values representing a “future value or error”. Each promise holds two collections of callbacks: one set to invoke if the promise “resolves” to a value; and another to invoke if the promise is “rejected” with an error.
They are useful for representing eventual completions of asynchronous tasks such as RPC.
Squeak’s promises follow the Promises/A+ specification as far as is possible within Squeak’s unique context.
More information on promises generally:
- Wikipedia on Futures and Promises
- Mozilla Developer Network on JavaScript Promises
- Promises/A+ specification
Promises from interactions with Actors
Every non-asynchronous ActorRequest includes a
Promise object (specifically, an ActorPromise).
The promise is used by the callee to reply to the caller with either a
value or an error, and by the caller to wait for and retrieve the
value or error.
If p is a Promise object, then
- if
p isResolved, thenp wait(or, equivalently,p value) yields its value. - if
p isRejected, thenp errorValueis its error value; otherwisep errorValueisnil. - if
pis neither resolved nor rejected, thenpis pending.
Waiting for a promise to be resolved or rejected
Use Promise >> #wait, Promise >> #waitTimeoutMSecs:, and
ActorPromise >> #waitFor:ifTimedOut: to block until a promise is
either resolved or rejected.
-
p waitreturnsp’s value as soon aspis resolved, or signals the result ofp errorValueifpis rejected. -
p waitTimeoutMSecs: mswaits for at mostmsmilliseconds forpto become resolved or rejected. It returnstrueifpis resolved, andfalseifpis rejected or the timeout expires. -
p waitFor: ms ifTimedOut: aBlockwaits for at mostmsmilliseconds forpto become resolved or rejected. It returnsp’s value ifpis resolved, signalsp errorValueifpis rejected, or returnsaBlock valueif the timeout expires.
Adding a continuation to a Promise
Use the operator >>= to attach a continuation to a Promise.
(some operationYieldingAPromise) >>= [:v | v + 1]
In this example, a continuation block [:v | v + 1] is attached to
the promise returned by #operationYieldingAPromise—call it “promise
A”—and a new promise, “promise B” is returned.
Make sure to use bindActor in conjunction with >>= if your
continuation is supposed to run “inside” your actor.
See below.
When promise A is resolved with a value, the continuation block is invoked, and its result is used to resolve promise B.
If promise A is rejected, promise B is also rejected (with the error value from promise A).
The >>= operator takes advantage of Smalltalk’s left-associativity
of binary operators, allowing “stacking” of continuations:
(some operationYieldingAPromise)
>>= [:v | v + 1]
>>= [:v | v * 99]
The >>= operator is punningly, though inaccurately, named after the
monadic bind operator seen in some functional programming languages.
It’s not quite a bind operator for the same reason that then isn’t a
bind operator in Promises/A+. It’s an
odd, reflective hybrid of bind and fmap.
Where and when do handlers run?
By default, callbacks registered with a Promise run in the process
that is resolving or rejecting the promise.
In an actor system, this is often not the right thing.
The problem
For example, consider the following method on a behavior object:
doSomethingWith: anActorProxy
(anActorProxy compute)
>>= [:result | self pvtHandleResult: result]
When the promise returned by compute resolves, the continuation
block will execute.
However, the private method of the calling actor will run as part of
the called actor’s process! If the calling actor—that is, the one
implementing doSomethingWith:—happens to be running at the same
time, we have a race condition.
The solution
Use BlockClosure >> #bindActor to transform an 0-ary or 1-ary block
into an equivalent object that ensures that the block will run as part
of the actor that called bindActor.
A correct version of the incorrect example above:
doSomethingWith: anActorProxy
(anActorProxy doSomething)
>>= [:result | self pvtHandleResult: result] bindActor
Could we design a general mechanism that avoids this kind of problem?
What about storing an optional Process along with each resolver and
rejecter in a Promise instance? If a Process slot is non-nil,
we’d have some method like executePromiseCallback: aBlock with:
aValue on various kinds of Process that automatically did the right
thing. If a Process slot is nil, the callback would be executed on
the process that happened to be running at the time.
ActorPromise
Every time a proxy kicks off a “synchronous” or “blocking” RPC request, it constructs a promise to receive the result of the RPC.
It specifically uses instances of ActorPromise, which inherits from
the Promise class included with Squeak.
ActorPromise augments the default Promise waiting behavior with
special treatment of ActorProcesses, which must continue to receive
system-level messages while they wait.
It relies on the fact that just such a system-level message is the
means by which each request is answered. This ensures that the waiting
ActorProcess is woken up when the promise becomes fulfilled.