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 value
is its value; otherwisep value
isnil
. - if
p isRejected
, thenp error
is its error value; otherwisep error
isnil
. - if
p
is neither resolved nor rejected, thenp
is 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 wait
returnsp value
as soon asp
is resolved, or signalsBrokenPromise
ifp
is rejected. -
p waitTimeoutMSecs: ms
waits for at mostms
milliseconds forp
to become resolved or rejected. It returnstrue
ifp
is resolved, andfalse
ifp
is rejected or the timeout expires. -
p waitFor: ms ifTimedOut: aBlock
waits for at mostms
milliseconds forp
to become resolved or rejected. It returnsp value
ifp
is resolved, signalsBrokenPromise
ifp
is rejected, or returnsaBlock value
if 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 ActorProcess
es, 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.