-
Notifications
You must be signed in to change notification settings - Fork 208
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
build durable notifier #4567
Comments
That trick looks like the way to go: // create these handles only in version-1, otherwise get from baggage
const counterHandle = makeKindHandle('counter');
const notifierHandle = makeKindHandle('notifier');
// in all versions:
// define a durable counter
const makeCounter = defineDurableKind(counterHandle, () => { c: 0 }, {
next: ({state}) => { state.c += state.c; return state.c; },
});
const counter = makeCounter();
// define an ephemeral table, indexed by the counter:
const ephemeralPromises = new Map();
function getEphemeralPromiseKit(index) {
if (!ephemeralPromises.has(index)) {
ephemeralPromises.set(index, makePromiseKit());
}
return ephemeralPromises.get(index);
}
// each new Notifier needs a distinct index
function initNotifierState() {
return { index: counter.next(), currentUpdateCount: undefined, currentResponse: undefined };
}
// define behavior for notifier
function getUpdateSince({ state }, updateCount = NaN) {
const { index, currentUpdateCount, currentResponse } = state;
const hasState = currentResponse !== undefined;
const final = currentUpdateCount === undefined;
if (hasState && (final || currentResponse && currentResponse.updateCount !== updateCount)) {
assert(currentResponse !== undefined);
return Promise.resolve(currentResponse);
}
// otherwise return a promise for the next state
const pk = provideEphemeralPromiseKit(index);
return pk.promise;
}
function updateState({ state }, value) {
const { index, currentUpdateCount } = state;
currentUpdateCount += 1;
state.currentUpdateCount = currentUpdateCount
const currentResponse = harden({ value,
updateCount: currentUpdateCount });
state.currentResponse = currentResponse;
if (ephemeralPromises.has(index)) {
ephemeralPromises.get(index).resolve(currentResponse);
ephemeralPromises.delete(index);
}
}
const makeNotifierKit = defineDurableKind(notifierHandle, initNotifierState, {
notifier: { getUpdateSince },
updater: { updateState },
}); There are lots of details to figure out, but the basic idea is that we remember the state, but not any promises that we've returned. The upgrade process will reject all promises made by the earlier version, which means clients of this DurableNotifier will see rejections upon upgrade. They must react to that by coming back to the Notifier (durable) and using The RAM needs are O(N) in the number of notifiers for which there are any outstanding |
Why not use a scalar virtual weak map store, with the durable object as key, and the promise as value? |
That would work too, and would probably be cleaner. But note that it doesn't save any RAM: if you put a Promise (or a Remotable) in a virtual store, the collectionManager must keep that ephemeral object alive with a refcounting table. |
Right, the only RAM saving solution is to have virtual/durable promises, and make sure to avoid introducing native promises in the chain. While I believe that's possible, it's a discussion for another day. |
Is there a way to make the rejection distinctive so the client can tell that it's an interruption in the stream and not some other kind fo dead notifier? |
While the discussion has veered wildly off-topic, I believe that was the goal of #5185 to find a reliable way. |
Do we still need this for MN-1 on upgrade can we refresh the notifier from its source? |
I think this ticket has (appropriately) mutated into "please provide a durable notifier", which internally uses the close over virtual table trick. Because the notifier is durable, it can be held by durable contracts/etc without problems. I'll update the title. When @gibson042 and @FUDCo and I were talking about this, we figured that it would introduce a new constraint: durable notifiers can only publish durable data. The requirement would arise from a goal of "the first @dtribble said we don't really need that: it would be ok for the first |
We sketched out one possible API: handle = provideDurableNotifierKitHandle(baggage, keyname);
const { notifier, updater } = makeDurableNotifierKit(handle, ...initialState); or maybe: const makeDurableNotifierKit = provideDurableNotifierKitMaker(baggage, keyname);
const { notifier, updater} = makeDurableNotifierKit(...initialState); (but note that @FUDCo was not a fan of either) As with other durablization work, we're seeing a pattern emerging: as a caller, if you want a library to give you durable objects, you must provide it with some durable baggage (where it can record at least the KindHandle that it uses for the durable Kind, perhaps other data). We want to keep the number of durable Kinds to a minimum, both because each one requires some amount of RAM (the instances are virtual+durable, but the Kinds themselves are not), and because the next incarnation of the vat is obligated to re-attach behavior for each one. So it must not be the case that each call to But given the challenge of coordinating multiple users of durable notifiers, and our appropriate reluctance to use impure module-level state (e.g. a module-level
However we should promote a pattern where |
@mhofman asked:
Oh, I realized a better answer why we need The resolve/reject functions are not methods of a Passable object, they're just bare functions, and we don't have a Promises can acquire identities, so they can be serialized. But that doesn't make them durable (yet]): liveslots carefully hangs on to the ephemeral It may indeed be slightly tidier to use a WeakMap (or While it feels a bit cleaner to use the Updater as the key, rather than the Notifier, it probably doesn't matter in practice, because I think both will be facets of the same durable-object cohort, which means they'll keep each other alive (along with the |
What is the Problem Being Solved?
As part of #4550 and #4383, we're faced with a challenge. Promises can be referenced by virtual objects (#2485). The
Promise
object remains in RAM, which doesn't meet our memory-consumption goals, but it remains sound to reference them. However they are not "durable", because they cannot be entirely ejected from RAM and then resurrected on-demand later. So durable data cannot reference virtual data, and only virtual data can reference promises.We know that we need to virtualize Notifiers (#4513) to meet our RAM goals. We know that Zoe and contracts need to hold their long-term state in durable data, and any externally-visible objects must be Durable (e.g. contract facets must survive upgrade). So the question is how clients should get reconnected to their notifiers after an upgrade.
One option is to make durable notifiers. Then the notifier can be part of the
state
of the durable facet/etc. However each notifier needs to hold onto a promise (the one it will resolve upon the next update), and promises cannot be durable. If we can create virtual promises (#3787) then we aren't violating our RAM budget, but we still can't reference the merely-virtual promise from the durable notifer'sstate
.Another is to leave notifiers as merely virtual. But the durable facet cannot reference such an object from its
state
.A trick I've been thinking about that might be useful in either approach would be to have the durable facet Kind constructor close over a merely-virtual Notifier constructor. They could share an integer index, and use it to reference a virtual table of notifiers that were created within this version of the vat. These notifiers would be lost upon upgrade, but regenerated when clients notice their
getUpdateSince()
promises rejecting and then ask for a new notifier.Description of the Design
Use an ephemeral table of promises, which get rejected upon upgrade: see below.
Security Considerations
Test Plan
The text was updated successfully, but these errors were encountered: