Agoric / agoric-sdk

monorepo for the Agoric Javascript smart contract platform
Apache License 2.0
326 stars 206 forks source link

"virtual promises" #3787

Open warner opened 3 years ago

warner commented 3 years ago

What is the Problem Being Solved?

In the recent testnet (phase4.5 "metering"), a significant number of lingering c-list entries were Promises, rather than objects. We've developed the "virtual object" mechanism to move the RAM costs of their data off into the DB, however we don't have a way to tolerate a large number of long-lived Promises within a vat.

Description of the Design

We're thinking of a special function, similar to the makeKind() used to prepare virtual objects. For purposes of discussion, let's name it makeVirtualPromise(). It would return a pair of Representatives. The first would have a method named resolve, the second would have methods for subscription (like .then and .catch, but not using those names). These Representatives are backed by virtual objects that will survive the Representatives themselves being dropped.

The general idea is that the (virtual-object) Purse would retain a reference to the resolution facet. Both could be dropped from RAM, but when something causes the Purse to be revived (a new Representative constructed), the resolution facet would be revived too, and could be invoked. The subscription facet could be sent to a remote vat, which could use it like a Promise. Perhaps the subscription facet should arrive as a Promise (as if we sent a real Promise to begin with), although that raises interesting round-trip issues.

cc @FUDCo @michaelfig @mhofman @dtribble

Security Considerations

Test Plan

warner commented 3 years ago

cc @erights , who pointed out that it might be better to address the specific needs of @agoric/notifier's Notifier (lossy) and Subscription (lossless), rather than a more generic Promise.

FUDCo commented 3 years ago

It strikes me that we might still have some dangling GC issues with regular (i.e., non-virtual) promises.

More generally, never getting around to resolving a promise can be a kind of storage leak.

zarutian commented 3 years ago

I propose the name Forgotten Promise Proplem, which sounds like it is mysterious,o f ancient lore and something that can be meta-ized by telling anyone curious about it that you will explain it later and then not following up on it. I think @erights would get a kick out of that.

Frankly, "virtual promises" sounds like euphemism for politicans promises.

warner commented 2 years ago

Thiknig some more on this. Maybe { vp, vr } = VatData.makeVirtualPromise(), then:

With this tool, you could:

You could not:

So the virtual promise has the same general design as a real Promise, but you can't use it in the same way, except for sending it in an argument and returning it from an externally-triggered (dispatch.deliver) object method (brokered by liveslots). But in exchange for that, you can store it in virtualized data without consuming RAM until it gets used.

That might be enough for a Notifier. I worry that it would look pretty weird, and not achieve our goals of "it's just JavaScript".

erights commented 2 years ago

That might be enough for a Notifier. I worry that it would look pretty weird, and not achieve our goals of "it's just JavaScript".

Pronoun ambiguity: Do you mean

I genuinely don't know which you mean, but I agree strongly with the second.

OTOH, when I start thinking about virtual promises representatives being genuine promises (or even just thenables), I get vertigo. Did you explore and reject this possibility?

michaelfig commented 2 years ago

OTOH, when I start thinking about virtual promises representatives being genuine promises (or even just thenables), I get vertigo. Did you explore and reject this possibility?

I can smell a solution in this direction if we flesh out support for Virtual Far functions (where myFarObj.method would be trivially convertible to a virtual far function).

warner commented 2 years ago

Mostly the second. The existing Notifier/Subscriptions tool is pretty easy to understand and qualifies as "just JavaScript". If we could encapsulate the virtualization weirdness inside some alternate "VirtualNotifier", such that contract code which hosts a notifier merely has to do a s/Notifier/VirtualNotifier/ replacement, that'd be nearly as good. But if you can't do a local getUpdateSince on it (because that would create a real Promise that couldn't be virtualized), then we start to drift away from "just JavaScript".

OTOH, when I start thinking about virtual promises representatives being genuine promises (or even just thenables), I get vertigo. Did you explore and reject this possibility?

I didn't even consider that: way beyond my comfort zone.

warner commented 2 years ago

I can smell a solution in this direction if we flesh out support for Virtual Far functions (where myFarObj.method would be trivially convertible to a virtual far function).

I thought about that, but I have no idea how that would work. The reason virtual objects work is because we can rebuild them on demand. "virtual functions" would require the same ability (some sort of makeKind-registered factory that can regenerate a single callable function on demand, from some serialized state). If the thing that needs to be virtualized is the resolve that we got back from makePromiseKit, I don't know how to rebuild one on demand, unless the deserializer conspires with makePromiseKit or something.

michaelfig commented 2 years ago

This is what I was thinking. It needs review for feasibility and usefulness.

"virtual functions" would require the same ability (some sort of makeKind-registered factory that can regenerate a single callable function on demand, from some serialized state).

Agreed. I'm talking about a general ability to "peel off" virtual functions from a virtual object, rather than needing to have a different pathway for defining individual virtual functions. But this really requires @erights.

If the thing that needs to be virtualized is the resolve that we got back from makePromiseKit, I don't know how to rebuild one on demand, unless the deserializer conspires with makePromiseKit or something.

I think in order to have compatible-with-Promises behaviour, we'd need a VatData.VirtualPromise. We'd also need to virtualise things on both the executor (which receives virtual functions for its resolve/reject/resolve-with-presence arguments), and also on the consumer side (which receives a virtual thenable).

virtualThenable = new VatData.VirtualPromise(executor)

could create a virtual object executorArgs = Virtual({ resolve, reject, resolveWithPresence }), and then provide the virtual functions as arguments to the above executor:

executor(peelOff(executorArgs, 'resolve'), peelOff(executorArgs, 'reject'), peelOff(executorArgs, 'resolveWithPresence').

That would give virtual functions on the resolver side. As far as virtual promises on the consumer side, we'd do something like:

virtualThenable = Virtual({ then, catch, finally }) with catch and finally implemented in terms of then.

those virtual thenables could remain virtual as long as the callback argument(s) supplied to then were virtual functions. If non-virtual callbacks were supplied, the virtual thenable would need to remain pinned in memory.

warner commented 2 years ago

@michaelfig and I walked through some of this today. Our vague plan is:

With this scheme, a durable Notifier could create and return durable Promises to do its job. Receivers of promises could use E.when() to register durable callbacks on them, then drop the Promise, without incurring memory consumption until resolution.

warner commented 2 years ago

Note to self: w.r.t. vat upgrade and the cleanup we must do during dispatch.stopVat (mostly described in https://github.com/Agoric/agoric-sdk/issues/1848#issuecomment-1074322765), we want to reject any Promises that are created by userspace (non-virtual/durable Promises), because once the RAM image is gone, there's no longer any code that can resolve/reject those. But we should refrain from rejecting the durable promises which can still be resolved/rejected by references that survive the upgrade.

zarutian commented 2 years ago
  • instrument HandledPromise with an own-property then method, allowing the promise creator to learn if/when someone calls .then on the promise (and thus cares about its resolution)

    • hold the resolve/reject pair in a WeakMap keyed by the Promise object (perhaps in a callback that is run if/when then() is called)

    • if then is called, register the vpid for lookup during dispatch.notify so resolve/reject can be called at that time

    • and do syscall.subscribe at the same time

Yes, please as it would fit ocapn way of listening onto promises.

warner commented 2 years ago

4932 would implement the durable nameless callable methods/functions that we'd need to have the most pleasant API for virtual/durable promises.

mhofman commented 2 years ago

Here is a quick attempt at something like virtual/durable promises: https://gist.github.com/mhofman/3b04d2e2275f7b17bece718fd32df898

michaelfig commented 2 years ago

Here is a quick attempt at something like virtual/durable promises: https://gist.github.com/mhofman/3b04d2e2275f7b17bece718fd32df898

I tried to digest what was going on in that gist, but it was too complex for me to understand the forest for the trees. Would you be able to provide a walkthrough/list of features that your solution has so that I can have some context as to what it attempts to accomplish?

mhofman commented 2 years ago

I've rewritten the gist with more comments and full typing. That might help a little.

The description of the types should help but the TLDR is:

Now passes Promises/A+ Compliance Test Suite!

warner commented 2 years ago

We're deferring this one for now, we'll allow notifiers to be durable by using the #4567 trick to hold ephemeral at-least-one-per-version promises instead.

mhofman commented 1 year ago

So here is my strawman:

Then to complete the loop we need to ensure vattp supports resolving a promise to another, which we've been putting off. That way liveslots can inform a result promise (which it imported but became the decider by getting the delivery), has been forwarded to the durable/virtual promise that liveslots observes as the direct result of the function call to userland it makes for the delivery. The biggest complication I see here is that liveslots relies on HandledPromise.applyMethod / HandledPromise.applyFunction to make the userland call, which I believes internally wraps the promise.

Pseudo-promise is where all these wrapping/unwrapping complications get solved, with these flowing through platform promise chains so we can figure out where these adoptions occur.