[E-Lang] Promises, equality and trust
Mark S. Miller
markm@caplet.com
Mon, 13 Aug 2001 23:08:21 -0700
At 02:54 PM Thursday 7/26/01, Mark Seaborn wrote:
>When they
>resolve, promises *become* the reference they resolved to, rather than
>just being [wrapping objects, like Scheme promises]. [list of advantages...]
>
> This advantage seems to be thrown away by using the Miranda
> method whenResolved for detecting resolution, since the object a
> promise resolved to may tell the observer that the promise resolved
> to something else. eg.
>
> when(X) -> done(Y) { Z } ...
>
> When Z is executed, X will be resolved. What, then, is the point
> of Y? If promises are a primitive provided by the vat, why isn't
> there a per-vat whenResolved function instead of a Miranda method?
Thanks for probing this issue. As a result of these questions, and of
addressing other issues in the new captp system, the answer (starting in
0.8.10) is that, when Z is executed, X and Y will indeed be identical, even
if the object they designate overrides whenMoreResolved in an aberrant way.
This is true even though the when-catch construct is still implemented using
whenMoreResolved. To understand this, let's expand the when-catch construct.
The when-catch construct's expansion to Kernel-E is unchanged from 0.8.9t. To expand on your example,
when (X) -> done(Y) {
...Z...
} catch problem {
...Handler...
}
expands to
Ref whenResolved(X, def done(temp) {
if (Ref isBroken(temp)) {
def problem := Ref optProblem(temp)
...Handler...
} else {
def Y := temp
...Z...
}
})
So we see that the when-catch construct is rather shallow syntactic sugar
around Ref's whenResolved/2
http://www.erights.org/javadoc/org/erights/e/elib/ref/Ref.html#whenResolved(java.lang.Object,%20org.erights.e.elib.util.OneArgFunc)
, which does the real work. Everything to the right of the "->" is turned
into the definition of the one-arg function that will be handed to Ref's
whenResolved/2. This function is used in an observer-like way. Observers
in E are called "reactors", so let's refer to this "done" function as the
"reactor". When does whenResolved/2 invoke the reactor? What does it
invoke the reactor with? The javadoc-umentation at the above URL (which
uses "ref" for the first "X" argument) says:
>If ref never becomes resolved, the reactor is not invoked. Should ref become
>resolved, the reactor will be invoked exactly once. For example, if ref
>becomes fulfilled and then broken, the reactor will hear of exactly one of
>these events.
>
>Once ref becomes resolved the reactor will be invoked with the resolution.
>Should the reactor be invoked with a non-broken value (and therefore a
>fulfilled value), all earlier messages sent on ref before the whenResolved
>are guaranteed to have been successfully delivered.
This text certainly corresponds to the normal assumptions under cooperative
conditions. What if the object designated by ref or the vat hosting the
object are hostile and seek to disrupt these guarantees. How bad can it
get? To explore this, I'll walk through the current draft implementation of
whenResolved/2. For layering reasons, this is actually implemented in Java,
but for clarity I'll translate to E.
def Ref {
# ...
to whenResolved(ref, reactor) {
def wrappingReactor := WhenResolvedReactorMaker new(reactor, ref)
ref <- whenMoreResolved(wrappingReactor)
}
}
where WhenResolvedReactorMaker is implemented equivalently to:
class WhenResolvedReactorMaker(var optWrapped, var ref) :any {
def WhenResolvedReactor {
to run(_) {
if (optWrapped != null) {
if (Ref isResolved(ref)) {
def tempRef := ref
def tempWrapped := optWrapped
ref := null
optWrapped := null
tempWrapped(tempRef)
} else {
ref <- whenMoreResolved(WhenResolvedReactor)
}
}
}
to reactToLostClient(_) {
WhenResolvedReactor(null) # invokes run/1 on itself
}
}
}
and recall that the Miranda method for whenMoreResolved is equivalent to
def Self {
# ...
to whenMoreResolved(reactor) {
reactor <- run(Self)
}
}
Wherever a defining occurrence of a variable is expected, an "_" may be used
instead as an "ignore pattern". This means the two methods of
WhenResolvedReactor don't care what argument they're provided. Therefore, a
malevolent invoker cannot cause mischief by providing a bad argument. Also,
we see that the two methods are equivalent, so no mischief can be caused by
invoking the wrong method. Remaining sources of mischief would only be to
invoke one of these functions when it shouldn't, or not to invoke it when it
should.
If these methods ignore their argument, what purpose do the corresponding
messages serve? In this context, they serve purely as a wakeup call to tell
the WhenResolvedReactor to check if the original ref has become resolved
yet. If it has, then it calls optWrapped -- the original done function -- on
this original (and now resolved) reference.
A malicious X cannot cause damage by invoking these methods when it
shouldn't. Once optWrapped is called, it's dropped, and so will not be
called more than once. If these methods are invoked before ref is resolved,
the done function isn't called or dropped, and a new whenMoreResolved
message is sent (to re-request a wakeup call).
What about the guarantee that the done function will not be invoked until
all earlier messages sent on X have arrived? If a malicious X could arrange
for the WhenResolvedReactor to be invoked after the reference is resolved,
but before these earlier messages have arrived, then it could indeed disrupt
our guarantees. But it can't, since it won't get the whenMoreResolved
message which gives it access to the WhenResolvedReactor until these earlier
messages have arrived.
What about a malicious vat hosting a malicious X? For these purposes,
analysis must proceed
http://www.erights.org/elib/capability/ode/ode-protocol.html#subj-aggregate
by considering the vat as a whole as a composite, with X as a facet. CapTP
will not provide the malicious vat access to the WhenResolvedReactor until
all prior messages meant for X have arrived at that vat.
Frankly, I don't know whether the safety provided by the last two paragraphs
makes any difference at all. However, since we are safe in this regard, I
don't need to figure out whether it would make a difference if we weren't.
This is an important design principle: it's often easier to provide a
stronger guarantee, rather than figure out if there would be a problem with
a weaker one.
A malicious X (or its hosting vat) can cause a denial of wakeup by not
invoking the run method when it should. The result would be that the done
function is not invoked. We could enhance the implementation so that the
WhenResolvedReactor were also registered as an observer directly on ref, but
then it would be woken up before prior messages had been delivered to X.
Because this prior message guarantee is so useful, we choose to keep this
property at the price of a possible denial of wakeup. Perhaps we should
revisit this decision.
Another kind of "malice" if you will is the unreliable network sitting
between vats that might be trying to cooperate. Even if X and its hosting
vat wish to play by the whenMoreResolved rules, their run/1 response may not
make it back to the WhenResolvedReactor because of a network partition. To
deal with this, the WhenResolvedReactor is also a DeadManSwitch, ie, an
object that responds to reactToLostClient/1
http://www.erights.org/javadoc/org/erights/e/elib/prim/MirandaMethods.html#reactToLostClient(java.lang.Object,%20java.lang.Throwable)
. This will cause it to also eventually wake up on a network partition that
could have prevented such a message from being delivered. Since ref would
have also been a remote reference to X, if ref were not already resolved,
such a partition would also break ref. Therefore neither accidental nor
induced partitions can cause a violation of our guarantees.
Ironically, a partition will also cure any attempted denial of wakeups.
>If the invocation of observers passed in whenResolved messages is the
>way promises get resolved to values, wouldn't that mean promises can
>get resolved to different values in different vats? A promise that
>started out being equal to itself would end up splitting into
>inconsistent values.
As we've already discussed, this issue is unavoidable between vats. Based
on the mechanism we explain above, we now perfectly avoid this issue within
a vat. How?
An important capability design pattern: Want to avoid getting inconsistent
answers to the same question? Only ask the question once, and only
"believe" the first answer. For a given unresolved reference in a given
vat, that vat must ask at least once "What's the resolution of this
reference?" Because each vat must ask separately, they can be told different
answers from each other, hence the unavoidable problem.
Within a vat, a captp unresolved remote reference automatically generates
whenMoreResolved messages for answering this question. Only one of these
answers will be used to resolve the remote reference (ie, the arrow-tail
side of the reference). Normal programs are encouraged to ask the question
using when-catch or Ref's whenResolved/2, and, by the above analysis, they
thereby share the answer "believed" by the remote reference itself.
Once again, thanks for asking. It took awhile to get this right, and
figuring out how to answer your questions helped us think about the issues.
Cheers,
--MarkM