Open warner opened 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.
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.
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.
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.
vp
in virtualized data.vp.then(function)
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.
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.vr
in virtualized datavr.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
exportWith this tool, you could:
vp
in virtualized data, to give it to someone again later, without consuming RAM in the meanwhilevr
in virtualized data, to retain the ability to resolve it later without consuming RAM until thendispatch.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
vr.resolve
is usedYou could not:
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.then
locally
vp.getPromise()
which returned a real Promise
, and resolve it at the same time as doing syscall.resolve
and firing the .thenSend
s, but then we must hold the virtual promise's representative in RAM until resolved, as a place to retain the real resolve
and reject
functionsSo 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".
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?
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).
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.
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.
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 frommakePromiseKit
, I don't know how to rebuild one on demand, unless the deserializer conspires withmakePromiseKit
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.
@michaelfig and I walked through some of this today. Our vague plan is:
f = VatData.DurableFarFunction(durableObject, methodname)
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 awarenessHandledPromise
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)vpid
s in the arguments of inbound dispatch.deliver
or dispatch.notify
deliveries):
syscall.subscribe
: defer it until .then
is called and we know somebody caresresolve
/reject
pair in a WeakMap keyed by the Promise object (perhaps in a callback that is run if/when then()
is called)then
is called, register the vpid for lookup during dispatch.notify
so resolve/reject can be called at that timedefineKind
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
{ promise, resolve, reject } = VatData.makeDurablePromiseKit()
, which does:
resolve
and reject
syscall.send
them into the kernel (who will queue them for us)p2 = VatData.when(p, resolveFarFunc, rejectFarFunc)
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
)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.
.then
, being in RAM, will not survive a vat upgradeE.when
will survive an upgradedispatch.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.
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.
instrument
HandledPromise
with an own-propertythen
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/whenthen()
is called)if
then
is called, register the vpid for lookup duringdispatch.notify
so resolve/reject can be called at that timeand do syscall.subscribe at the same time
Yes, please as it would fit ocapn way of listening onto promises.
Here is a quick attempt at something like virtual/durable promises: https://gist.github.com/mhofman/3b04d2e2275f7b17bece718fd32df898
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?
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:
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.Now passes Promises/A+ Compliance Test Suite!
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.
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 } }
kitwatchPromise
requires that the promise either be not decided locally, or a virtual/durable promise (which is decided "locally")
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.
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 itmakeVirtualPromise()
. It would return a pair of Representatives. The first would have a method namedresolve
, 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