proposal-eventual-send icon indicating copy to clipboard operation
proposal-eventual-send copied to clipboard

Handling immediate invocations and or accesses?

Open zarutian opened this issue 5 years ago • 13 comments

Hi there.

What happens with immediate invocations and access on HandledPromises?

Are errors thrown or traps on the handler invoked?

If the latter could the usual Proxy handler traps be used? Perhaps the names of those traps have "immediate_" prefix to prevent name collisions. The default behaviour of those traps would be to throw an error such as "Immedite invocation attempted on a HandledPromise". (well, one exception would be in applyMethod invocation of .then(), .catch(), and .finally() on the handledPromise)

-Zarutian

zarutian avatar Jan 24 '20 22:01 zarutian

Hi,

Are you thinking of HandledPromises where there are methods added to the object itself? For the current @agoric/eventual-send shim, under SES HandledPromises are born frozen, so there are no immediate invocations other than the things in Promise.prototype.

We don't want to allow the interception of invocations or data access, as that could be a vector for plan interference on the client side.

If this is not what you're talking about, can you provide an example?

michaelfig avatar Jan 26 '20 22:01 michaelfig

Hmm, I see now where I might have missunderstood where to apply the the implementation ideas of far refs from E.

Looking at https://github.com/Agoric/agoric-sdk/blob/master/packages/eventual-send/src/index.js I see that the resolveWithPresence (which is passed to the executor, the function passed as first argument to new HandledPromise()) is intended for that kind of implementation use. However, it seems that aforementioned resolveWithPresence function creates the presence object instead of taking one as its second argument. This prevents one from using a Proxy as the presence.

The ?invocation signiture? of resolveWithPresence was not documented in this proposal at the time this issue was opened. I offer «resolveWithPresence(handlerOfEventualSendsToThePresence, presence = Object.create(null))» as such signiture. Where handlerOfEventualSendsToThePresence is exactly as structured as the unfulfilledHandler passed to new HandledPromise.

zarutian avatar Jan 27 '20 06:01 zarutian

I offer «resolveWithPresence(handlerOfEventualSendsToThePresence, presence = Object.create(null))»

Okay, thanks, that's a lot clearer. I understand your desire for a presence that is also a proxy. That's not the way we're using them in the Agoric platform, but I think such a possibility is reasonable. That's provided we make the implementation defensive to ensure presence is not already used elsewhere as a presence.

@erights, your thoughts?

michaelfig avatar Jan 27 '20 15:01 michaelfig

However, it seems that aforementioned resolveWithPresence function creates the presence object instead of taking one as its second argument.

Yes. We used to take a presence as an argument. But then we realized that this created a global communications channel. For example, if one provided the shared Array constructor as the alleged presence. We protect against that by enforcing that the presence must be fresh, which we ensure by creating it ourselves.

This prevents one from using a Proxy as the presence.

Two possible answers:

Have it make the proxy for you.

Would could instead extend the resolveWithPresence function with two optional parameters:

resolveWithPresence(presenceHandler, proxyTarget = undefined, proxyHandler = undefined)

If both of these parameters are present, then resolveWithPresence could make the proxy/presence itself, still ensuring it is fresh. However, this is a huge hammer for what seems like an obscure problem. My inclination is that it doesn't pay for itself. But I could be convinced, perhaps by Una (attn @FUDCo), that proxy/presences in fact need to be well supported.

Make the presence inherit from a proxy.

const presence = resolveWithPresence(presenceHandler);
const proxy = new Proxy(proxyTarget, proxyHandler);
Object.setPrototypeOf(presence, proxy);
Object.freeze(presence);  // freeze while empty

The presence will inherit from the proxy. This is not as expressive. An object that inherits from a proxy is not as flexible as the proxy itself. But it works around the limitation to some degree, at some complexity cost for this obscure case, without making the API more complicated for everyone else.

erights avatar Jan 28 '20 02:01 erights

I think that having the resolveWithPresence make the proxy for you offers the most flexibility in implementing various kinds of presences such as Una, Croquet-replicas, and other such genre of distributed objects.

Here is a modified version of part of the shim:

    const resolveWithPresence = (presenceHandler, proxyTarget = undefined, proxyHandler = undefined) => {
      if (fulfilled) {
        return resolvedPresence;
      }
      try {
      // Sanity checks.
      validateHandler(presenceHandler);

      // Validate and install our mapped target (i.e. presence).
      if (proxyTarget !== undefined) {
        if (proxyHandler === undefined) {
          throw new Error("proxyHandler must not be undefined");
        } else {
          resolvedPresence = new Proxy(proxyTarget, proxyHandler)
        }
      } else {
        resolvedPresence = Object.create(null);
      }

      // Create table entries for the presence mapped to the
      // fulfilledHandler.
      presenceToPromise.set(resolvedPresence, handledP);
      promiseToPresence.set(handledP, resolvedPresence);
      presenceToHandler.set(resolvedPresence, presenceHandler);

      // Remove the mapping, as our presenceHandler should be
      // used instead.
      promiseToHandler.delete(handledP);

      // We committed to this presence, so resolve.
      handledResolve(resolvedPresence);
      continueForwarding();
      return resolvedPresence;
    } catch (e) {
      handledReject(e);
      continueForwarding();
      throw e;
    }
  };

As you see, it is not as much of a huge hammer as it looks at first glance. (9 additional lines plus one changed). I also suspect that this is the most complication of the API this issue causes.

zarutian avatar Jan 28 '20 15:01 zarutian

This looks reasonable. I'd prefer to add an options bag, where options.proxy describes the proxy:

Maybe something like:

const resolveWithPresence = (presenceHandler, options = {})
[...]
const { proxy: proxyOpts, resultCallback } = options;
let result;
if (proxyOpts) {
  const { args, revocable } = proxyOpts;
  if (revocable) {
    // Create a proxy and its revoke function.
    result = Proxy.revocable(...args);
    presence = result.proxy;
  } else {
    result = new Proxy(...args);
    presence = result;
  }
} else {
  // Default presence.
  result = Object.create(null);
  presence = result;
}

// Provide the resulting proxy (or {proxy, revoke} object).
if (resultCallback) {
  resultCallback(result);
}

I'm not attached to the above design, just thinking that if we want to enable proxies, we might as well go all the way.

michaelfig avatar Jan 29 '20 15:01 michaelfig

@michaelfig I like the idea of adding an options bag, but not yet adding these until we find a need. For the una cases I'm thinking about, we'd have a fixed set of methods, which we can add to the presence using defineOwnProperty. I'd like to see an example where it is useful for the presence to be a proxy.

erights avatar Jan 30 '20 05:01 erights

One example where it is useful for the presence to be a proxy is the implementation of a kind of membrane for what I call CroquetReplicatedState. I can boil it down to, explanation-wise, membrane proxies that allow immediate readonly access but only allow mutation via eventual sends. (The eventual sends are all sent as messages to an „eye of an hourglass” for the purposes of global event sequencing. Those sends are then broadcasted back to all the replicas.)

As these membrane proxies do not know ahead of read accesses which props will be asked for it is difficult, for the implementer, to anticipate those programatically. I suppose heavy introspection into the target each such proxy is fronting for might be an uneasy workaround.

zarutian avatar Feb 04 '20 18:02 zarutian

Coincidentally, I've been working on my talk "preserve host virtualizability" for the current tc39 meeting. This made more more receptive. We should do this. But there remains the issue of timing: when to propose it, and when to add it to our shim. Do you expect to make any immediate use of this if it were available?

erights avatar Feb 04 '20 21:02 erights

No, not immediate, as in this quarter of the year but soon-ish (during the summer, maybe), use but it will make farref, una, and replica implementations easier. (far refs would err on immediate accesses (beside .then, .catch, .finally, and, possibly .there) in more informative way)

zarutian avatar Feb 16 '20 01:02 zarutian

By far ref do you mean the remote promise or its local presence? The reason I ask is that .then, etc, should normally only be an issue for promises, not presences.

erights avatar Feb 16 '20 01:02 erights

Its local presence. That is, the object often depicted as a half circle in object graph diagrams that show the graph spanning multiple vats.

zarutian avatar Feb 19 '20 12:02 zarutian

Ok, let's move ahead with this. @michaelfig , your code at https://github.com/tc39/proposal-eventual-send/issues/8#issuecomment-579824005 seems fine except for one thing. Both Proxy.revocable and new Proxy are currently defined to take exactly two arguments: target and handler. Given this, it seems overkill to pass a proxy.args option instead of proxy.handler and proxy.target options.

With this change, it you'd like to turn the code in that comment into a PR, I'll happily review it. Thanks.

erights avatar Feb 21 '20 00:02 erights