Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"virtual promises" #3787

Open
warner opened this issue Sep 2, 2021 · 18 comments
Open

"virtual promises" #3787

warner opened this issue Sep 2, 2021 · 18 comments
Labels
enhancement New feature or request liveslots requires vat-upgrade to deploy changes SwingSet package: SwingSet

Comments

@warner
Copy link
Member

warner commented Sep 2, 2021

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 warner added enhancement New feature or request SwingSet package: SwingSet labels Sep 2, 2021
@warner
Copy link
Member Author

warner commented Sep 2, 2021

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
Copy link
Contributor

FUDCo commented Sep 2, 2021

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

  • Vat A sends promise to Vat B. Our messaging system implicitly subscribes Vat B to the promise, but if the code inside Vat B simply drops the promise reference on the floor, it won't get cleaned up until Vat A resolves it, which might leave it sitting for much longer than might otherwise be necessary (though it will eventually get cleaned up on resolution).

  • Vat A sends promise to Vat B, then drops it. Meanwhile, Vat B is still subscribed and I don't recall that our finalizer machinery is used on exported promises. The local promise in Vat A will get cleaned up by regular GC, but I'm not sure Vat B will ever hear about it.

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

@zarutian
Copy link
Contributor

zarutian commented Sep 3, 2021

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
Copy link
Member Author

warner commented Feb 10, 2022

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

  • vp is the virtual Promise: you can send it to another vat, and they'll receive a normal Promise, on which they can use .then as usual. Their Promise is real: it will consume RAM that can't be shed until the promise is resolved.
    • You can store vp in virtualized data.
    • You cannot use vp.then(function)
    • but, we could provide a vp.thenSend(target, methodname), and we'd record the target's vref and the string methodname you picked. We can record this in virtualized data.
    • vp would get a new kind of vref (maybe vp+NN), so we can store it in virtualized data, but this vref would not leave the vat. When translating in a syscall.send, it would get replaced with a p+NN value. The kernel doesn't see virtual promises, only normal promise IDs.
  • vr is the resolution object.
    • This would get a new kind of vref (maybe vr+NN, which cannot leave the vat). Or maybe we define a built-in Kind for this, so it could leave the vat (as o+N/NN, if you were foolish enough to share the resolution authority), but the Representatives we build for it have special knowledge of the vpid they're resolving.
    • you can store vr in virtualized data
    • vr.resolve(data) or vr.reject(data) can be called, once, and it fires both any local .thenSend invocations and does a syscall.resolve() to notify the kernel about the matching p+NN export

With this tool, you could:

  • manually create a new VirtualPromise and send it to someone in a message argument, or inside the data of another promise resolution
  • store the vp in virtualized data, to give it to someone again later, without consuming RAM in the meanwhile
  • store the vr in virtualized data, to retain the ability to resolve it later without consuming RAM until then
  • we could change the way dispatch.deliver works to recognize return vp and record the result= vpid in the virtualized promise data, so that when vr.resolve is used, liveslots does a syscall.resolve
    • that would avoid RAM usage until vr.resolve is used

You could not:

  • use it to reduce RAM usage on a received (real) Promise
    • liveslots will create a real Promise object upon receipt of a p-NN vref, and once that's created, it's stuck in RAM (by liveslots, if not userspace) until resolved
    • we could build a virtual promise around the real one, but the real one would still stick around in RAM, so there's not much point
  • use it to reduce RAM usage on promises that you must .then locally
    • we could add a vp.getPromise() which returned a real Promise, and resolve it at the same time as doing syscall.resolve and firing the .thenSends, but then we must hold the virtual promise's representative in RAM until resolved, as a place to retain the real resolve and reject functions

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
Copy link
Member

erights commented Feb 11, 2022

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
* If we only have virtual Notifiers/Subscriptions, not virtual promises, that 'would look pretty weird, and not achieve our goals of "it's just JavaScript".'
* If we create a promise-like abstraction that is not only not a promise but not even a thenable, that 'would look pretty weird, and not achieve our goals of "it's just JavaScript".'

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
Copy link
Member

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
Copy link
Member Author

warner commented Feb 13, 2022

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
Copy link
Member Author

warner commented Feb 13, 2022

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
Copy link
Member

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
Copy link
Member Author

warner commented Mar 25, 2022

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

  • somehow implement "Durable Far Functions", something like f = VatData.DurableFarFunction(durableObject, methodname)
    • internally, this would build a vref that combines the object's vref plus the method name
    • building one from a virtual-object Representative/Facet is the easiest case
    • it would be nice to be able to build one from a Presence and a method name, but the vref-kref-vref mapping is tougher, and the kernel would need to be aware
      • we can imagine a importer-side library function that does E.get(presence).methodname and basically asks the exporter to build one for us, which could cost a (pipelineable) roundtrip but wouldn't require kernel awareness
  • 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)
  • change liveslots' handling of imported Promises (vpids in the arguments of inbound dispatch.deliver or dispatch.notify deliveries):
    • don't automatically call syscall.subscribe: defer it until .then is called and we know somebody cares
    • 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
  • use an internal defineKind to create a category of durable virtual objects, one per virtual/durable promise, which have a .resolve and .reject method, tentatively named "Execution Objects". The init() argument is a vpid string. The state contains a status and a list of durable far functions to invoke as callbacks or errbacks
    • this Kind cannot be created by userspace because resolve/reject need access to the liveslots tables and the DB
  • define { promise, resolve, reject } = VatData.makeDurablePromiseKit(), which does:
    • allocate a vpid, create a new (virtual/durable) Execution Object around the vpid
    • create a new Promise (registered under the vpid as if we just imported a promise in a delivery)
    • obtain durable far functions for resolve and reject
    • return the trio to userspace
  • this Promise has a durable vpid and can be stored in durable vdata or exported to other vats
    • other vats can send message to the promise and they will be queued in the kernel's promise table
    • if the local userspace sends messages to the promise, we should syscall.send them into the kernel (who will queue them for us)
    • or, if we implement some durable message queueing within the vat, we could store these pending messages in the vat's DB (and we could accept pipelined messages from other vats and store them in the DB too)
  • Implement p2 = VatData.when(p, resolveFarFunc, rejectFarFunc)
    • if p is a durable promise, and the functions are durable far functions, record the callbacks in the DB indexed by p's vpid. Otherwise behave like E.when(p, ..) (which is a safer form of p.then)
  • Userspace can send, stash, or E.when their durable promise, and they won't incur a memory hit. But if they call .then (or await), liveslots will need to register the real resolve/reject functions, and will thus keep the promise around in RAM until it is resolved.
    • callbacks attached with .then, being in RAM, will not survive a vat upgrade
    • durable callbacks attached with E.when will survive an upgrade
  • when liveslots receives dispatch.notify, it will look up the vpid in the DB to see if there are any durable callbacks that need to be executed (and maybe if there are any durable queued messages that need to be delivered to the resolution). It will also look at the in-RAM table to see if there are non-durable RAM callbacks that need to be fired (meaning either the real Promise's resolve or reject method is invoked)

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
Copy link
Member Author

warner commented Mar 25, 2022

Note to self: w.r.t. vat upgrade and the cleanup we must do during dispatch.stopVat (mostly described in #1848 (comment)), 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
Copy link
Contributor

  • 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
Copy link
Member Author

warner commented Mar 26, 2022

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

@mhofman
Copy link
Member

mhofman commented Apr 9, 2022

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

@michaelfig
Copy link
Member

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
Copy link
Member

mhofman commented Apr 10, 2022

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:

  • A Virtual Promise is a virtual object facet, sharing state with a "settler" facet (containing the resolve/reject methods), and an internal facet (handles adding watchers)
  • The same virtual "watcher" concept as in register handlers for durable promises #5006 to handle reactions, but with the ability to chain.
  • A generalized virtualWatch helper which is basically the combination of Promise.resolve() for coercing any value to a virtual promise, and promise.then() to add a virtual watcher for reactions to a coerced value.
  • A special "redirector" virtual watcher to settle a promise to the resolution/rejection of another promise. (This is currently reactive and doesn't attempt to shortcut a chain of redirections)
  • The state of a promise is it status (pending/redirected/fulfilled/rejected), the resolution/rejection value and a list of watchers
  • Virtual promises are "then-able", allowing seamless integration with native promises (with the caveat that they'd lose their ability to get evicted)

Now passes Promises/A+ Compliance Test Suite!

@warner
Copy link
Member Author

warner commented Apr 12, 2022

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.

@warner warner added the liveslots requires vat-upgrade to deploy changes label Jan 24, 2023
@mhofman
Copy link
Member

mhofman commented Sep 1, 2023

So here is my strawman:

  • VatData.makeVirtualPromiseKit / VatData.makeDurablePromiseKit, which each return a { promise: Promise<T>, resolvers: { resolve: (value: T) => void, reject: (reason: any) => void } } kit
  • Liveslots internally implements the resolver kinds.
  • When creating the promise kit, liveslots adds a virtual/durable mapping from the vpid to the vref of the resolvers
  • watchPromise requires that the promise either be not decided locally, or a virtual/durable promise (which is decided "locally")
    • The decided check is to avoid footguns where a durable/virtual promise gets adopted by a bare platform promise, which we're currently unable to track
      • The check doesn't cause a synchronous failure, but a rejection of the watcher (to support future pseudo promise unwrapping)
    • There is no way for the program to use a virtual/durable promise as the result of a send, which would be the only way to change away the decider vat
    • However another vat can effectively become the decider of a virtual/durable promise by exporting the resolver object, but that wouldn't be represented by a different state of the promise
  • when upgrading, liveslots:
    • goes through all virtual promises, and rejects them
    • goes through all durable promises, and reject any where the resolver is no longer referenced (vref missing)
    • goes through all remaining decided promises (non virtual and non durable) and rejects them
      • An imported promise may have become watched before the vat became the decider

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request liveslots requires vat-upgrade to deploy changes SwingSet package: SwingSet
Projects
None yet
Development

No branches or pull requests

7 participants