From c02253cf94fb5f087e81ac0bb5c1bc5a1bf53587 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2023 12:38:28 -0600 Subject: [PATCH 01/25] fix(swingset): add placeholders for upgrade/stopvat work This makes room inside processUpgradeVat() for the new cleanup steps that we want to add. refs #6650 refs #7001 refs #6694 refs #6696 --- packages/SwingSet/src/kernel/kernel.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 1884af60894..3c18f803653 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -823,6 +823,11 @@ export default function buildKernel( const vd1 = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd1); const status1 = await deliverAndLogToVat(vatID, kd1, vd1); + // TODO: send BOYD to the vat, to give it one last chance to clean + // up, drop imports, and delete durable data. If we ever have a + // vat that is so broken it can't do BOYD, we can make that + // optional. #7001 + // make arguments for vat-vat-admin.js vatUpgradeCallback() /** * @param {SwingSetCapData} _errorCD @@ -873,8 +878,17 @@ export default function buildKernel( return results; } - // stopVat succeeded, so now we stop the worker, delete the - // transcript and any snapshot + // stopVat succeeded. finish cleanup on behalf of the worker. + + // TODO: walk c-list for all decided promises, reject them all #6694 + + // TODO: getNonDurableObjectExports, synthesize abandonVSO, + // execute it as if it were a syscall. (maybe distinguish between + // reachable/recognizable exports, abandon the reachable, retire + // the recognizable) #6696 + + // cleanup done, now we stop the worker, delete the transcript and + // any snapshot await vatWarehouse.resetWorker(vatID); const source = { bundleID }; From 3efce4e7c2502cb3f5a2342bd032658eb56c8c1f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2023 12:46:31 -0600 Subject: [PATCH 02/25] fix(swingset): factor out getDecidedPromises() cleanupAfterTerminatedVat() had a loop which identified all the promises (kpids) which were decided by a vat, so it could reject all of them as the vat was being terminated. We will have a need for this list of kpids during vat upgrade too, so refactor it out into a separate kernelKeeper method, and change terminateVat() to fetch the list before calling cleanupAfterTerminatedVat(). --- packages/SwingSet/src/kernel/kernel.js | 6 ++- .../SwingSet/src/kernel/state/kernelKeeper.js | 44 ++++++++++--------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 3c18f803653..28447a6a412 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -257,7 +257,8 @@ export default function buildKernel( // check will report 'false'. That's fine, there's no state to // clean up. if (kernelKeeper.vatIsAlive(vatID)) { - const promisesToReject = kernelKeeper.cleanupAfterTerminatedVat(vatID); + const promisesToReject = kernelKeeper.getDecidedPromises(vatID); + kernelKeeper.cleanupAfterTerminatedVat(vatID); for (const kpid of promisesToReject) { resolveToError(kpid, makeError('vat terminated'), vatID); } @@ -818,8 +819,9 @@ export default function buildKernel( upgradeMessage, incarnationNumber: vatKeeper.getIncarnationNumber(), }; + const disconnectObjectCD = kser(disconnectObject); /** @type { import('../types-external.js').KernelDeliveryStopVat } */ - const kd1 = harden(['stopVat', kser(disconnectObject)]); + const kd1 = harden(['stopVat', disconnectObjectCD]); const vd1 = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd1); const status1 = await deliverAndLogToVat(vatID, kd1, vd1); diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 4cdc9d84664..2a77c76643c 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -775,14 +775,32 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { kvStore.set(`${kernelSlot}.data.slots`, capdata.slots.join(',')); } + function* getDecidedPromises(vatID) { + const promisePrefix = `${vatID}.c.p`; + for (const k of enumeratePrefixedKeys(kvStore, promisePrefix)) { + // The vpid for a promise imported or exported by a vat (and thus + // potentially a promise for which the vat *might* be the decider) will + // always be of the form `p+NN` or `p-NN`. The corresponding vpid->kpid + // c-list entry will thus always begin with `vMM.c.p`. Decider-ship is + // independent of whether the promise was imported or exported, so we + // have to look up the corresponding kernel promise table entry to see + // whether the vat is the decider or not. If it is, we add the promise + // to the list of promises that must be rejected because the dead vat + // will never be able to act upon them. + const kpid = kvStore.get(k); + const p = getKernelPromise(kpid); + if (p.state === 'unresolved' && p.decider === vatID) { + yield kpid; + } + } + } + function cleanupAfterTerminatedVat(vatID) { insistVatID(vatID); // eslint-disable-next-line no-use-before-define const vatKeeper = provideVatKeeper(vatID); const exportPrefix = `${vatID}.c.o+`; const importPrefix = `${vatID}.c.o-`; - const promisePrefix = `${vatID}.c.p`; - const kernelPromisesToReject = []; vatKeeper.deleteSnapshotsAndTranscript(); @@ -822,23 +840,8 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { // that will also delete both db keys } - // now find all orphaned promises, which must be rejected - for (const k of enumeratePrefixedKeys(kvStore, promisePrefix)) { - // The vpid for a promise imported or exported by a vat (and thus - // potentially a promise for which the vat *might* be the decider) will - // always be of the form `p+NN` or `p-NN`. The corresponding vpid->kpid - // c-list entry will thus always begin with `vMM.c.p`. Decider-ship is - // independent of whether the promise was imported or exported, so we - // have to look up the corresponding kernel promise table entry to see - // whether the vat is the decider or not. If it is, we add the promise - // to the list of promises that must be rejected because the dead vat - // will never be able to act upon them. - const kpid = kvStore.get(k); - const p = getKernelPromise(kpid); - if (p.state === 'unresolved' && p.decider === vatID) { - kernelPromisesToReject.push(kpid); - } - } + // the caller used getDecidedPromises() before calling us, so they + // already known the orphaned promises, so it can reject them // now loop back through everything and delete it all for (const k of enumeratePrefixedKeys(kvStore, `${vatID}.`)) { @@ -870,8 +873,6 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { } decStat('vats'); } - - return kernelPromisesToReject; } function addMessageToPromiseQueue(kernelSlot, msg) { @@ -1597,6 +1598,7 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { provideVatKeeper, vatIsAlive, evictVatKeeper, + getDecidedPromises, cleanupAfterTerminatedVat, addDynamicVatID, getDynamicVats, From d79623f3fb3b87653dba1c71eb1153711c9d962c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2023 12:50:05 -0600 Subject: [PATCH 03/25] fix: move rejectAllPromises from stopVat to kernels-side upgradeVat When upgrading a vat, we must reject all promises that were decided by the old incarnation, because none of them are durable (the new incarnation will be unable to resolve them). Previously, the vat's stopVat() delivery would do the rejection. This moves the responsibility to the kernel, to happen in the pause between the old version being shut down and the new version being launched. closes #6694 --- packages/SwingSet/src/kernel/kernel.js | 5 ++++- packages/swingset-liveslots/src/stop-vat.js | 24 --------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 28447a6a412..91ddf41a1a8 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -882,7 +882,10 @@ export default function buildKernel( // stopVat succeeded. finish cleanup on behalf of the worker. - // TODO: walk c-list for all decided promises, reject them all #6694 + // walk c-list for all decided promises, reject them all + for (const kpid of vatKeeper.getDecidedPromises()) { + doResolve(vatID, [[kpid, true, disconnectObjectCD]]); + } // TODO: getNonDurableObjectExports, synthesize abandonVSO, // execute it as if it were a syscall. (maybe distinguish between diff --git a/packages/swingset-liveslots/src/stop-vat.js b/packages/swingset-liveslots/src/stop-vat.js index 488fe2af920..a522b5db99e 100644 --- a/packages/swingset-liveslots/src/stop-vat.js +++ b/packages/swingset-liveslots/src/stop-vat.js @@ -33,25 +33,6 @@ import { enumerateKeysWithPrefix } from './vatstore-iterators.js'; const rootSlot = makeVatSlot('object', true, 0n); -function rejectAllPromises({ deciderVPIDs, syscall, disconnectObjectCapData }) { - // Pretend that userspace rejected all non-durable promises. We - // basically do the same thing that `thenReject(p, vpid)(rejection)` - // would have done, but we skip ahead to the syscall.resolve - // part. The real `thenReject` also does pRec.reject(), which would - // give control to userspace (who might have re-imported the promise - // and attached a .then to it), and stopVat() must not allow - // userspace to gain agency. - - const rejections = deciderVPIDs.map(vpid => [ - vpid, - true, - disconnectObjectCapData, - ]); - if (rejections.length) { - syscall.resolve(rejections); - } -} - function identifyExportedRemotables( vrefSet, { exportedRemotables, valToSlot }, @@ -298,11 +279,6 @@ function deleteCollectionsWithDecref({ syscall, vrm }) { // END: the preceding functions aren't ready for use yet export async function releaseOldState(tools) { - // First, pretend that userspace has rejected all non-durable - // promises, so we'll resolve them into the kernel (and retire their - // IDs). - - rejectAllPromises(tools); // The next step is to pretend that the kernel has dropped all // non-durable exports: both the in-RAM Remotables and the on-disk From 24016f039898b4149105cc6b1dfe916cbb99e745 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 3 Mar 2023 21:02:33 -0500 Subject: [PATCH 04/25] fix(SwingSet): Access getDecidedPromises on the correct object --- packages/SwingSet/src/kernel/kernel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 91ddf41a1a8..754db43fc83 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -883,7 +883,7 @@ export default function buildKernel( // stopVat succeeded. finish cleanup on behalf of the worker. // walk c-list for all decided promises, reject them all - for (const kpid of vatKeeper.getDecidedPromises()) { + for (const kpid of kernelKeeper.getDecidedPromises(vatID)) { doResolve(vatID, [[kpid, true, disconnectObjectCD]]); } From 517ab2802eff1992a25ec027b111a01b0625da39 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 7 Mar 2023 17:23:54 -0500 Subject: [PATCH 05/25] fix(SwingSet): Iterate kpids decided by a vat before removing its data --- packages/SwingSet/src/kernel/kernel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 754db43fc83..a93a809bd4a 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -257,7 +257,9 @@ export default function buildKernel( // check will report 'false'. That's fine, there's no state to // clean up. if (kernelKeeper.vatIsAlive(vatID)) { - const promisesToReject = kernelKeeper.getDecidedPromises(vatID); + // Reject all promises decided by the vat, making sure to capture the list + // of kpids before that data is deleted. + const promisesToReject = [...kernelKeeper.getDecidedPromises(vatID)]; kernelKeeper.cleanupAfterTerminatedVat(vatID); for (const kpid of promisesToReject) { resolveToError(kpid, makeError('vat terminated'), vatID); From 4189e9af57d99830ad1aa34a714c33f115343ff1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 7 Mar 2023 17:30:46 -0500 Subject: [PATCH 06/25] style(SwingSet): Colocate getDecidedPromises with related functions --- .../SwingSet/src/kernel/state/kernelKeeper.js | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 2a77c76643c..499840cb2ef 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -775,26 +775,6 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { kvStore.set(`${kernelSlot}.data.slots`, capdata.slots.join(',')); } - function* getDecidedPromises(vatID) { - const promisePrefix = `${vatID}.c.p`; - for (const k of enumeratePrefixedKeys(kvStore, promisePrefix)) { - // The vpid for a promise imported or exported by a vat (and thus - // potentially a promise for which the vat *might* be the decider) will - // always be of the form `p+NN` or `p-NN`. The corresponding vpid->kpid - // c-list entry will thus always begin with `vMM.c.p`. Decider-ship is - // independent of whether the promise was imported or exported, so we - // have to look up the corresponding kernel promise table entry to see - // whether the vat is the decider or not. If it is, we add the promise - // to the list of promises that must be rejected because the dead vat - // will never be able to act upon them. - const kpid = kvStore.get(k); - const p = getKernelPromise(kpid); - if (p.state === 'unresolved' && p.decider === vatID) { - yield kpid; - } - } - } - function cleanupAfterTerminatedVat(vatID) { insistVatID(vatID); // eslint-disable-next-line no-use-before-define @@ -926,6 +906,26 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { kvStore.set(`${kpid}.decider`, ''); } + function* getDecidedPromises(vatID) { + const promisePrefix = `${vatID}.c.p`; + for (const k of enumeratePrefixedKeys(kvStore, promisePrefix)) { + // The vpid for a promise imported or exported by a vat (and thus + // potentially a promise for which the vat *might* be the decider) will + // always be of the form `p+NN` or `p-NN`. The corresponding vpid->kpid + // c-list entry will thus always begin with `vMM.c.p`. Decider-ship is + // independent of whether the promise was imported or exported, so we + // have to look up the corresponding kernel promise table entry to see + // whether the vat is the decider or not. If it is, we add the promise + // to the list of promises that must be rejected because the dead vat + // will never be able to act upon them. + const kpid = kvStore.get(k); + const p = getKernelPromise(kpid); + if (p.state === 'unresolved' && p.decider === vatID) { + yield kpid; + } + } + } + function addSubscriberToPromise(kernelSlot, vatID) { insistKernelType('promise', kernelSlot); insistVatID(vatID); @@ -1569,6 +1569,7 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { addSubscriberToPromise, setDecider, clearDecider, + getDecidedPromises, incrementRefCount, decrementRefCount, getObjectRefCount, @@ -1598,7 +1599,6 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { provideVatKeeper, vatIsAlive, evictVatKeeper, - getDecidedPromises, cleanupAfterTerminatedVat, addDynamicVatID, getDynamicVats, From f0ee093e84e824954371e2f677d0786be205e9c4 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 7 Mar 2023 17:58:40 -0500 Subject: [PATCH 07/25] style(SwingSet): Align promise-by-decider generator with related functions --- packages/SwingSet/src/kernel/kernel.js | 6 +++--- packages/SwingSet/src/kernel/state/kernelKeeper.js | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index a93a809bd4a..f91463d2f0e 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -259,9 +259,9 @@ export default function buildKernel( if (kernelKeeper.vatIsAlive(vatID)) { // Reject all promises decided by the vat, making sure to capture the list // of kpids before that data is deleted. - const promisesToReject = [...kernelKeeper.getDecidedPromises(vatID)]; + const deadPromises = [...kernelKeeper.enumeratePromisesByDecider(vatID)]; kernelKeeper.cleanupAfterTerminatedVat(vatID); - for (const kpid of promisesToReject) { + for (const kpid of deadPromises) { resolveToError(kpid, makeError('vat terminated'), vatID); } } @@ -885,7 +885,7 @@ export default function buildKernel( // stopVat succeeded. finish cleanup on behalf of the worker. // walk c-list for all decided promises, reject them all - for (const kpid of kernelKeeper.getDecidedPromises(vatID)) { + for (const kpid of kernelKeeper.enumeratePromisesByDecider(vatID)) { doResolve(vatID, [[kpid, true, disconnectObjectCD]]); } diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 499840cb2ef..e3afc63d10d 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -820,8 +820,8 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { // that will also delete both db keys } - // the caller used getDecidedPromises() before calling us, so they - // already known the orphaned promises, so it can reject them + // the caller used enumeratePromisesByDecider() before calling us, + // so they already know the orphaned promises to reject // now loop back through everything and delete it all for (const k of enumeratePrefixedKeys(kvStore, `${vatID}.`)) { @@ -906,7 +906,8 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { kvStore.set(`${kpid}.decider`, ''); } - function* getDecidedPromises(vatID) { + function* enumeratePromisesByDecider(vatID) { + insistVatID(vatID); const promisePrefix = `${vatID}.c.p`; for (const k of enumeratePrefixedKeys(kvStore, promisePrefix)) { // The vpid for a promise imported or exported by a vat (and thus @@ -1569,7 +1570,7 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { addSubscriberToPromise, setDecider, clearDecider, - getDecidedPromises, + enumeratePromisesByDecider, incrementRefCount, decrementRefCount, getObjectRefCount, From f4653983cc51e04adc165d28b5fe21d9453736aa Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 7 Mar 2023 17:59:24 -0500 Subject: [PATCH 08/25] test(SwingSet): Minor improvements --- packages/SwingSet/test/bootstrap-syscall-failure.js | 2 +- packages/SwingSet/test/test-marshal.js | 2 +- packages/SwingSet/test/vat-syscall-failure.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/SwingSet/test/bootstrap-syscall-failure.js b/packages/SwingSet/test/bootstrap-syscall-failure.js index e22cc98d98d..123e6b54fbc 100644 --- a/packages/SwingSet/test/bootstrap-syscall-failure.js +++ b/packages/SwingSet/test/bootstrap-syscall-failure.js @@ -34,7 +34,7 @@ export function buildRootObject(vatPowers, vatParameters) { () => testLog('p2 resolve (bad!)'), e => testLog(`p2 reject ${e}`), ); - const p3 = E(badvat).begood(ourThing); + const p3 = E(badvat).begoodagain(ourThing); p3.then( () => testLog('p3 resolve (bad!)'), e => testLog(`p3 reject ${e}`), diff --git a/packages/SwingSet/test/test-marshal.js b/packages/SwingSet/test/test-marshal.js index ccc6b6c7aad..16029f47390 100644 --- a/packages/SwingSet/test/test-marshal.js +++ b/packages/SwingSet/test/test-marshal.js @@ -144,7 +144,7 @@ test('unserialize promise', async t => { t.truthy(p instanceof Promise); }); -test('kernel serialzation of errors', async t => { +test('kernel serialization of errors', async t => { // The kernel synthesizes e.g. `Error('vat-upgrade failure')`, so we // need kmarshal to serialize those errors in a deterministic // way. This test checks that we don't get surprising things like diff --git a/packages/SwingSet/test/vat-syscall-failure.js b/packages/SwingSet/test/vat-syscall-failure.js index 7442ac1d163..861c73e399e 100644 --- a/packages/SwingSet/test/vat-syscall-failure.js +++ b/packages/SwingSet/test/vat-syscall-failure.js @@ -8,7 +8,7 @@ export default function setup(syscall, _state, _helpers, vatPowers) { } const { method, args } = extractMessage(vatDeliverObject); vatPowers.testLog(`${method}`); - const thing = method === 'begood' ? args.slots[0] : 'o-3414159'; + const thing = method.startsWith('begood') ? args.slots[0] : 'o-3414159'; syscall.send(thing, kser(['pretendToBeAThing', [method]])); } return dispatch; From 82b37a7fabaa25e9156ed3d8ca6ac477bf706c05 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 7 Mar 2023 18:39:02 -0500 Subject: [PATCH 09/25] style(SwingSet): Improve variable names and step ordering --- packages/SwingSet/src/kernel/kernel.js | 84 +++++++++++++++----------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index f91463d2f0e..248f2bfc617 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -821,39 +821,37 @@ export default function buildKernel( upgradeMessage, incarnationNumber: vatKeeper.getIncarnationNumber(), }; - const disconnectObjectCD = kser(disconnectObject); + const disconnectionCapData = kser(disconnectObject); /** @type { import('../types-external.js').KernelDeliveryStopVat } */ - const kd1 = harden(['stopVat', disconnectObjectCD]); - const vd1 = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd1); - const status1 = await deliverAndLogToVat(vatID, kd1, vd1); + const stopVatKD = harden(['stopVat', disconnectionCapData]); + const stopVatVD = vatWarehouse.kernelDeliveryToVatDelivery( + vatID, + stopVatKD, + ); + const stopVatStatus = await deliverAndLogToVat(vatID, stopVatKD, stopVatVD); + const stopVatResults = deliveryCrankResults(vatID, stopVatStatus, false); - // TODO: send BOYD to the vat, to give it one last chance to clean - // up, drop imports, and delete durable data. If we ever have a - // vat that is so broken it can't do BOYD, we can make that - // optional. #7001 + // We don't meter stopVat, since no user code is running, but we + // still report computrons to the runPolicy + let { computrons } = stopVatResults; // BigInt or undefined + if (computrons !== undefined) { + assert.typeof(computrons, 'bigint'); + } - // make arguments for vat-vat-admin.js vatUpgradeCallback() /** - * @param {SwingSetCapData} _errorCD + * Make a method-arguments structure representing failure + * for vat-vat-admin.js vatUpgradeCallback(). + * + * @param {SwingSetCapData} _errorCapData * @returns {RawMethargs} */ - function makeFailure(_errorCD) { - insistCapData(_errorCD); // kser(Error) + const makeFailureMethargs = _errorCapData => { + insistCapData(_errorCapData); // kser(Error) // const error = kunser(_errorCD) // actually we shouldn't reveal the details, so instead we do: const error = Error('vat-upgrade failure'); return ['vatUpgradeCallback', [upgradeID, false, error]]; - } - - // We use deliveryCrankResults to parse the stopVat status. - const results1 = deliveryCrankResults(vatID, status1, false); - - // We don't meter stopVat, since no user code is running, but we - // still report computrons to the runPolicy - let { computrons } = results1; // BigInt or undefined - if (computrons !== undefined) { - assert.typeof(computrons, 'bigint'); - } + }; // TODO: if/when we implement vat pause/suspend, and if // deliveryCrankResults changes to not use .terminate to indicate @@ -861,18 +859,20 @@ export default function buildKernel( // pause/suspend a vat for a delivery error, here we want to // unwind the upgrade. - if (results1.terminate) { + if (stopVatResults.terminate) { // get rid of the worker, so the next delivery to this vat will // re-create one from the previous state // eslint-disable-next-line @jessie.js/no-nested-await await vatWarehouse.stopWorker(vatID); // notify vat-admin of the failed upgrade - const vatAdminMethargs = makeFailure(results1.terminate.info); + const vatAdminMethargs = makeFailureMethargs( + stopVatResults.terminate.info, + ); // we still report computrons to the runPolicy const results = harden({ - ...results1, + ...stopVatResults, computrons, abort: true, // always unwind consumeMessage: true, // don't repeat the upgrade @@ -884,9 +884,14 @@ export default function buildKernel( // stopVat succeeded. finish cleanup on behalf of the worker. + // TODO: send BOYD to the vat, to give it one last chance to clean + // up, drop imports, and delete durable data. If we ever have a + // vat that is so broken it can't do BOYD, we can make that + // optional. #7001 + // walk c-list for all decided promises, reject them all for (const kpid of kernelKeeper.enumeratePromisesByDecider(vatID)) { - doResolve(vatID, [[kpid, true, disconnectObjectCD]]); + resolveToError(kpid, disconnectionCapData, vatID); } // TODO: getNonDurableObjectExports, synthesize abandonVSO, @@ -909,23 +914,32 @@ export default function buildKernel( // deliver a startVat with the new vatParameters /** @type { import('../types-external.js').KernelDeliveryStartVat } */ - const kd2 = harden(['startVat', vatParameters]); - const vd2 = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd2); + const startVatKD = harden(['startVat', vatParameters]); + const startVatVD = vatWarehouse.kernelDeliveryToVatDelivery( + vatID, + startVatKD, + ); // decref vatParameters now that translation did incref for (const kref of vatParameters.slots) { kernelKeeper.decrementRefCount(kref, 'upgrade-vat-event'); } - const status2 = await deliverAndLogToVat(vatID, kd2, vd2); - const results2 = deliveryCrankResults(vatID, status2, false); - computrons = addComputrons(computrons, results2.computrons); + const startVatStatus = await deliverAndLogToVat( + vatID, + startVatKD, + startVatVD, + ); + const startVatResults = deliveryCrankResults(vatID, startVatStatus, false); + computrons = addComputrons(computrons, startVatResults.computrons); - if (results2.terminate) { + if (startVatResults.terminate) { // unwind just like above // eslint-disable-next-line @jessie.js/no-nested-await await vatWarehouse.stopWorker(vatID); - const vatAdminMethargs = makeFailure(results2.terminate.info); + const vatAdminMethargs = makeFailureMethargs( + startVatResults.terminate.info, + ); const results = harden({ - ...results2, + ...startVatResults, computrons, abort: true, // always unwind consumeMessage: true, // don't repeat the upgrade From 9ce63bc491c276577f4a28e0fc4ba5ced1b54c08 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 8 Mar 2023 13:55:10 -0500 Subject: [PATCH 10/25] test(swingset-liveslots): Verify that vat upgrade rejects local promises --- packages/swingset-liveslots/package.json | 4 +- .../test/test-vat-upgrade.js | 73 +++++++++++++++++++ .../test/vat-durable-promise-watcher.js | 40 ++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 packages/swingset-liveslots/test/test-vat-upgrade.js create mode 100644 packages/swingset-liveslots/test/vat-durable-promise-watcher.js diff --git a/packages/swingset-liveslots/package.json b/packages/swingset-liveslots/package.json index 9efed053c92..0fa13f591e0 100644 --- a/packages/swingset-liveslots/package.json +++ b/packages/swingset-liveslots/package.json @@ -16,7 +16,9 @@ "lint:types": "tsc -p jsconfig.json", "lint:eslint": "eslint ." }, - "devDependencies": {}, + "devDependencies": { + "@agoric/swingset-vat": "^0.30.2" + }, "dependencies": { "@agoric/assert": "^0.5.1", "@agoric/internal": "^0.2.1", diff --git a/packages/swingset-liveslots/test/test-vat-upgrade.js b/packages/swingset-liveslots/test/test-vat-upgrade.js new file mode 100644 index 00000000000..d03a2ca3411 --- /dev/null +++ b/packages/swingset-liveslots/test/test-vat-upgrade.js @@ -0,0 +1,73 @@ +import '@agoric/swingset-vat/tools/prepare-test-env.js'; +import test from 'ava'; +import { buildVatController } from '@agoric/swingset-vat'; +import { kunser } from '@agoric/swingset-vat/src/lib/kmarshal.js'; + +const bfile = name => new URL(name, import.meta.url).pathname; + +test('local promises are rejected by vat upgrade', async t => { + // TODO: Generalize packages/SwingSet/test/upgrade/test-upgrade.js + /** @type {SwingSetConfig} */ + const config = { + includeDevDependencies: true, // for vat-data + defaultManagerType: 'xs-worker', + bootstrap: 'bootstrap', + defaultReapInterval: 'never', + vats: { + bootstrap: { + sourceSpec: bfile('../../SwingSet/test/bootstrap-relay.js'), + }, + }, + bundles: { + watcher: { sourceSpec: bfile('./vat-durable-promise-watcher.js') }, + }, + }; + const c = await buildVatController(config); + t.teardown(c.shutdown); + c.pinVatRoot('bootstrap'); + await c.run(); + + const run = async (method, args = []) => { + assert(Array.isArray(args)); + const kpid = c.queueToVatRoot('bootstrap', method, args); + await c.run(); + const status = c.kpStatus(kpid); + if (status === 'fulfilled') { + const result = c.kpResolution(kpid); + return kunser(result); + } + assert(status === 'rejected'); + const err = c.kpResolution(kpid); + throw kunser(err); + }; + const messageVat = (name, methodName, args) => + run('messageVat', [{ name, methodName, args }]); + // eslint-disable-next-line no-underscore-dangle + const _messageObject = (presence, methodName, args) => + run('messageVatObject', [{ presence, methodName, args }]); + + const S = Symbol.for('passable'); + await run('createVat', [{ name: 'watcher', bundleCapName: 'watcher' }]); + await messageVat('watcher', 'watchLocalPromise', ['orphaned']); + await messageVat('watcher', 'watchLocalPromise', ['fulfilled', S]); + await messageVat('watcher', 'watchLocalPromise', ['rejected', undefined, S]); + const v1Settlements = await messageVat('watcher', 'getSettlements'); + t.deepEqual(v1Settlements, { + fulfilled: { status: 'fulfilled', value: S }, + rejected: { status: 'rejected', reason: S }, + }); + await run('upgradeVat', [{ name: 'watcher', bundleCapName: 'watcher' }]); + const v2Settlements = await messageVat('watcher', 'getSettlements'); + t.deepEqual(v2Settlements, { + fulfilled: { status: 'fulfilled', value: S }, + rejected: { status: 'rejected', reason: S }, + orphaned: { + status: 'rejected', + reason: { + name: 'vatUpgraded', + upgradeMessage: 'vat upgraded', + incarnationNumber: 1, + }, + }, + }); +}); diff --git a/packages/swingset-liveslots/test/vat-durable-promise-watcher.js b/packages/swingset-liveslots/test/vat-durable-promise-watcher.js new file mode 100644 index 00000000000..aad122941dc --- /dev/null +++ b/packages/swingset-liveslots/test/vat-durable-promise-watcher.js @@ -0,0 +1,40 @@ +import { Far } from '@endo/marshal'; +import { getCopyMapEntries, M } from '@agoric/store'; +import { makePromiseKit } from '@endo/promise-kit'; +import { + prepareExo, + provideDurableMapStore, + watchPromise, +} from '@agoric/vat-data'; + +export function buildRootObject(_vatPowers, vatParameters, baggage) { + const settlements = provideDurableMapStore(baggage, 'settlements'); + const PromiseWatcherI = M.interface('PromiseWatcher', { + onFulfilled: M.call(M.any(), M.string()).returns(), + onRejected: M.call(M.any(), M.string()).returns(), + }); + const watcher = prepareExo(baggage, 'PromiseWatcher', PromiseWatcherI, { + onFulfilled(value, name) { + settlements.init(name, harden({ status: 'fulfilled', value })); + }, + onRejected(reason, name) { + settlements.init(name, harden({ status: 'rejected', reason })); + }, + }); + + return Far('root', { + watchLocalPromise: (name, fulfillment, rejection) => { + const { promise, resolve, reject } = makePromiseKit(); + if (fulfillment !== undefined) { + resolve(fulfillment); + } else if (rejection !== undefined) { + reject(rejection); + } + watchPromise(promise, watcher, name); + }, + getSettlements: () => { + const settlementsCopyMap = settlements.snapshot(); + return Object.fromEntries(getCopyMapEntries(settlementsCopyMap)); + }, + }); +} From a602f2cc4f682d1f29d3d0807d931184ae4858b7 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 8 Mar 2023 13:59:57 -0500 Subject: [PATCH 11/25] docs(swingset-liveslots): Clean up VPID managment documentation --- .../swingset-liveslots/src/vpid-tracking.md | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/packages/swingset-liveslots/src/vpid-tracking.md b/packages/swingset-liveslots/src/vpid-tracking.md index 7c32976a186..e8725c5360d 100644 --- a/packages/swingset-liveslots/src/vpid-tracking.md +++ b/packages/swingset-liveslots/src/vpid-tracking.md @@ -2,85 +2,88 @@ Kernels and vats communicate about promises by referring to their VPIDs: vat(-centric) promise IDs. These are strings like `p+12` and `p-23`. Like VOIDs (object IDs), the plus/minus sign indicates which side of the boundary allocated the number (`p+12` and `o+12` are allocated by the vat, `p-13` and `o-13` are allocated by the kernel). But where the object ID sign also indicates which side "owns" the object (i.e. where the behavior lives), the promise ID sign is generally irrelevant. -Instead, we care about which side holds the resolution authority. This is not indicated by the VPID sign, and in fact is not necessarily static. Liveslots does not currently have any mechanism to allow one promise to be forwarded to another, but if it acquires this some day, then the decider of a promise could shift from kernel to vat to kernel again before it finally gets resolved. And there *is* a sequence that allows a vat to receive a reference to a promise in method arguments first, then receive a message whose result promise uses that VPID second (`p1 = p2~.foo(); x~.bar(p1)`, then someone resolves `p2` to `x`). In this case, the decider is initially the kernel, then the authority is transferred to the vat later. +Instead, we care about which side holds the resolution authority for a promise (referred to as being its **decider**). This is not indicated by the VPID sign, and in fact is not necessarily static. Liveslots does not currently have any mechanism to allow one promise to be forwarded to another, but if it acquires this some day, then the decider of a promise could shift from kernel to vat to kernel again before it finally gets resolved. And there *are* sequences that allow a vat to receive a reference to a promise in method arguments before receiving a message for which that same VPID identifies the result promise (e.g., `pk = makePromiseKit(); p2 = E(pk.promise).push('queued'); await E(observer).push(p2); pk.resolve(observer)` with an observer whose `push` returns a prompt response). In such cases, the decider is initially the kernel but later becomes the receiving vat. -Each `Promise` starts out in the "unresolved" state, then later transitions irrevocably to the "resolved" state. Liveslots frequently (but not always) pays attention to this transition by calling `.then`, to attach a callback. To handle resolution cycles, liveslots remembers the resolution of old promises in a `WeakMap` for as long as the `Promise` exists. Consequently, for liveslots' purposes, every `Promise` is either resolved (the callback has fired and liveslots remembers the resolution), or unresolved (liveslots has not yet seen a resolution). +Each Promise starts out in the "unresolved" state, then later transitions irrevocably to the "resolved" state. Liveslots frequently (but not always) pays attention to this transition by calling `then` to attach fulfillment/rejection callbacks. To handle resolution cycles, liveslots remembers the resolution of old promises in a `WeakMap` for as long as the Promise exists. Consequently, for liveslots' purposes, every Promise is either resolved (a callback has fired and liveslots remembers the settlement), or unresolved (liveslots has not yet seen a resolution). There are roughly four ways that liveslots might become aware of a promise: -* serialization: a `Promise` instance is serialized, either for the arguments of an outbound `syscall.send` or `syscall.resolve`, the argument of `watchPromise()`, or to be stored into virtualized data (e.g. `bigMapStore.set(key, promise)`, or assignment to a property of a virtual object) -* creation for outbound result: liveslots allocates a VPID for the `.result` of an outbound `syscall.send`, and creates a new `Promise` instance to give back to userspace -* deserialization: the arguments of an inbound `dispatch.deliver` or `dispatch.notify` are deserialized, and a new `Promise` instance is created -* inbound result: the kernel-allocated `.result` VPID of an inbound `dispatch.deliver` is associated with the `Promise` we get back from `HandledPromise.applyMethod` +* serialization: a Promise instance is serialized, either for the arguments of an outbound `syscall.send` or `syscall.resolve`, the argument of `watchPromise()`, or to be stored into virtualized data (e.g. `bigMapStore.set(key, promise)`, or assignment to a property of a virtual object) +* creation for outbound result: liveslots allocates a VPID for the `result` of an outbound `syscall.send`, and creates a new Promise instance to give back to userspace +* deserialization: the arguments of an inbound `dispatch.deliver` or `dispatch.notify` are deserialized, and a new Promise instance is created +* inbound result: the kernel-allocated `result` VPID of an inbound `dispatch.deliver` is associated with the Promise we get back from `HandledPromise.applyMethod` -A `Promise` may be associated with a VPID even though the kernel does not know about it (i.e. the VPID is not in the c-list): this can occur when a Promise is merely stored into virtual data without also being sent to (or received from) the kernel. The kernel's knowledge is temporary: once the promise is resolved (either `syscall.resolve` or `dispatch.notify`), the VPID is retired from the vat's c-list. So a VPID might start out stored only in vdata, then get sent to the kernel, then get resolved, leaving it solely in vdata once more. +A Promise may be associated with a VPID even though the kernel does not know about it (i.e. the VPID is not in the kernel's c-list for that vat). This can occur when a Promise is stored into virtual data without also being sent to (or received from) the kernel. It can also occur when a Promise is resolved but still referenced in vdata (the kernel's knowledge is temporary; it retires VPIDs from c-lists upon `syscall.resolve` or `dispatch.notify` as relevant). So a VPID might start out stored only in vdata, then get sent to the kernel, then get resolved, leaving it solely in vdata once more. -Each unresolved VPID has a decider: either the vat or the kernel. The VPID is resolved by the first of the following events: +Each unresolved VPID has a decider: either the kernel or a vat. It can remain unresolved for arbitrarily long, but becomes resolved by the first of the following events: -* if/when userspace resolves the corresponding `Promise` and liveslots learns of the resolution, then liveslots will perform a `syscall.resolve()` -* if/when the vat is upgraded, liveslots will perform a `syscall.resolve()` of all remaining VPIDs during the delivery of `dispatch.stopVat()` (after which all `Promise`s evaporate along with the rest of the JS heap) -* if/when the vat is terminated, the kernel internally rejects all remaining vat-decided KPIDs, without involving the vat -* otherwise, the VPID remains unresolved until the heat death of the universe +* if userspace resolves the corresponding Promise and liveslots learns of the resolution, then liveslots will perform a `syscall.resolve()` +* if the vat is upgraded, liveslots will perform a `syscall.resolve()` of all remaining VPIDs during the delivery of `dispatch.stopVat()` (after which all Promises evaporate along with the rest of the JS heap) +* if the vat is terminated, the kernel internally rejects all remaining vat-decided KPIDs without involving the vat Liveslots tracks promises in the following data structures: -* `slotToVal` / `valToSlot` : these manage *registration*, the mapping from VPID to `Promise` and vice versa. These also register objects (Presences, Remotables, and Representatives) to/from VOIDs, and device nodes. +* `slotToVal` / `valToSlot` : these manage *registration*, the mapping from VPID to Promise and vice versa. These also register objects (Presences, Remotables, and Representatives) to/from VOIDs, and device nodes. * to support GC of objects, `slotToVal.get(vref)` is a WeakRef, and `valToSlot` is a WeakMap * liveslots uses independent strong references to maintain object/promise lifetimes * `exportedVPIDs`: a `Map`: all Promises currently known to the kernel and decided by the vat * `importedVPIDs`: a `Map`: all Promises currently known to the kernel and decided by the kernel * `remotableRefCounts`: a `Map`: all Promises (and Remotables) referenced by virtual data -The vat-kernel c-list contains all VPIDs in `exportedVPIDs` and `importedVPIDs`. The vat is the decider for `exportedVPIDs`, while the kernel is the decider for `importedVPIDs`. For every VPID in `exportedVPIDs`, we've used `.then` on the `Promise` instance to arrange for a `syscall.resolve` when the promise resolves or rejects. For every VPID key of the `importedVPIDs` Map, the corresponding value is a `[resolve, reject]` "`pRec`", so one of them can be called during `dispatch.notify`. Every VPID in `slotToVal` is either in `exportedVPIDs`, `importedVPIDs`, or neither. +The kernel's c-list for a vat contains all VPIDs in `exportedVPIDs` and `importedVPIDs`. The vat is the decider for `exportedVPIDs`, while the kernel is the decider for `importedVPIDs`. For every VPID in `exportedVPIDs`, we've used `then` on the Promise instance to arrange for a `syscall.resolve` when it settles (becomes fulfilled or rejected). For every VPID key of the `importedVPIDs` Map, the corresponding value is a `[resolve, reject]` "**pRec**", so one of the functions can be called during `dispatch.notify`. Every VPID in `slotToVal` is either in `exportedVPIDs` but not `importedVPIDs`, `importedVPIDs` but not `exportedVPIDs`, or neither. -If a VPID in `importedVPIDs` is resolved (by the kernel, via `dispatch.notify`), the VPID is removed from `importedVPIDs`. If a VPID in `exportedVPIDs` is resolved (by the vat, i.e. liveslots observes the previously-added `.then` callback be executed), liveslots invokes `syscall.resolve` and removes the VPID from `exportedVPIDs`. The c-list will not contain VPIDs for any resolved promise. +If a VPID in `importedVPIDs` is resolved (by the kernel, via `dispatch.notify`), the VPID is removed from `importedVPIDs`. If a VPID in `exportedVPIDs` is resolved (by the vat, i.e. liveslots observes invocation of a previously-added settlement callback), liveslots invokes `syscall.resolve` and removes the VPID from `exportedVPIDs`. The c-list for a vat will not contain a VPID for any resolved promise. -The `slotToVal`/`valToSlot` registration must remain until 1: the kernel is no longer aware of the VPID, 2: the Promise is not present in any virtual data, and 3: the promise is not being watched by a `promiseWatcher`. If the registration were to be lost while one of those three conditions were still true, a replacement `Promise` might be created while the original was still around, causing confusion. +The `slotToVal`/`valToSlot` registration must remain until all of the following are true: + +* the kernel is no longer aware of the VPID +* the Promise is not present in any virtual data +* the promise is not being watched by a `promiseWatcher`. + +If the registration were to be lost while any of the above conditions were still true, a replacement Promise might be created while the original was still around, causing confusion. ## Maintaining Strong References -Remember that the `slotToVal` registration uses a WeakRef, so being registered there does not keep the `Promise` object alive. +Remember that the `slotToVal` registration uses a WeakRef, so being registered there does not keep the Promise object alive. -`exportedVPIDs` and `importedVPIDs` keep their `Promise` alive in their value. vdata keeps it alive through the key of `remotableRefCounts`. `promiseWatcher` uses an internal `ScalarBigMapStore` to keep the `Promise` alive. +`exportedVPIDs` and `importedVPIDs` keep their Promise alive in their value. vdata keeps it alive through the key of `remotableRefCounts`. `promiseWatcher` uses an internal `ScalarBigMapStore` to keep the Promise alive. ## Promise/VPID Management Algorithm -* When a `Promise` is first serialized (it appears in `convertValToSlot`), a VPID is assigned and the VPID/`Promise` mapping is registered in `valToSlot`/`slotToVal` +* When a Promise is first serialized (it appears in `convertValToSlot`), a VPID is assigned and the VPID/Promise mapping is registered in `valToSlot`/`slotToVal` * at this point, there is not yet a strong reference to the Promise * When a VPID appears in the serialized arguments of `syscall.send` or `syscall.resolve`: * if the VPID already exists in `exportedVPIDs` or `importedVPIDs`: do nothing - * else: use `followForKernel` to add to `exportedVPIDs` and use `.then()` to attach a `handle` callback -* When `followForKernel`'s `handle` callback is executed: + * else: use `followForKernel` to add the VPID to `exportedVPIDs` and attach settlement callbacks +* When a `followForKernel` settlement callback is executed: * do `syscall.resolve()` * remove from `exportedVPIDs` - * check `remotableRefCounts`. If 0: unregister from `valToSlot`/`slotToVal` + * if `remotableRefCounts` reports 0 references: unregister from `valToSlot`/`slotToVal` * When the kernel delivers a `dispatch.notify`: - * retrieve the `resolve`/`reject` "`pRec`" from `importedVPIDs` + * retrieve the `[resolve, reject]` pRec from `importedVPIDs` * invoke the appropriate function with the deserialized argument - * check `remotableRefCounts`. If 0: unregister from `valToSlot`/`slotToVal` + * if `remotableRefCounts` reports 0 references: unregister from `valToSlot`/`slotToVal` * When the vdata refcount for a VPID drops to zero: * if the VPID still exists in `exportedVPIDs` or `importedVPIDs`: do nothing * else: unregister from `valToSlot`/`slotToVal` * When a new VPID is deserialized (it appears in `convertSlotToVal`), this must be the arguments of a delivery (not vdata) * use `makePipelinablePromise` to create a HandledPromise for the VPID - * add the Promise and it's `resolve`/`reject` pair to `importedVPIDs` - * register the Promise in `valToSlot`/`slotToVal'` + * add the Promise and its `resolve`/`reject` pair to `importedVPIDs` + * register the Promise in `valToSlot`/`slotToVal` * use `syscall.subscribe` to request a `dispatch.notify` delivery when the kernel resolves this promise -* When a VPID appears as the `.result` of an outbound `syscall.send`: +* When a VPID appears as the `result` of an outbound `syscall.send`: (_note overlap with the preceding_) * use `allocateVPID` to allocate a new VPID * use `makePipelinablePromise` to create a HandledPromise for the VPID - * add the Promise and it's `resolve`/`reject` pair to `importedVPIDs` - * register the Promise in `valToSlot`/`slotToVal'` + * add the Promise and its `resolve`/`reject` pair to `importedVPIDs` + * register the Promise in `valToSlot`/`slotToVal` * use `syscall.subscribe` to request a `dispatch.notify` delivery when the kernel resolves this promise -* When a VPID appears as the `.result` of an inbound `dispatch.deliver`: - * check to see if the VPID is present in `importedVPIDs`: - * if yes: extract `pRec`, use `pRec.resolve(res)` to forward the invocation result promise to the previously-imported promise, then remove the VPID from `importedVPIDs` - * if no: register `res` the invocation result promise under the VPID - * in either case, next we add the VPID to `exportedVPIDs` - * then we call `followForKernel` to attach a callback to the `res` promise +* When a VPID appears as the `result` of an inbound `dispatch.deliver`: + * if the VPID is present in `importedVPIDs`: retrieve the `[resolve, reject]` pRec and use `resolve(res)` to forward the invocation result promise to the previously-imported promise, then remove the VPID from `importedVPIDs` + * else: register `res` the invocation result promise under the VPID + * in either case, use `followForKernel` to add the VPID to `exportedVPIDs` and attach settlement callbacks -If the serialization is for storage in virtual data, the act of storing the `VPID` will add the `Promise` to `remotableRefCounts`, which maintains a strong reference for as long as the VPID is held. When it is removed from virtual data (or the object/collection is deleted), the refcount will be decremented. When the refcount drops to zero, we perform the `exportedVPIDs`/`importedVPIDs` check and then maybe unregister the promise. +If the serialization is for storage in virtual data, the act of storing the VPID will add the Promise to `remotableRefCounts`, which maintains a strong reference for as long as the VPID is held. When it is removed from virtual data (or the object/collection is deleted), the refcount will be decremented. When the refcount drops to zero, we perform the `exportedVPIDs`/`importedVPIDs` check and then maybe unregister the promise. If the serialization is for the arguments of an outbound `syscall.send` or `syscall.resolve` (or `syscall.callNow`, or `syscall.exit`), the VPID will be added to `exportedVPIDs`. From 5d841578aa2fbe92fe99a5f376b6a8d12475dfc0 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 8 Mar 2023 21:07:26 -0500 Subject: [PATCH 12/25] refactor(swingset-liveslots): Update some function signatures Increases clarity and convenience and sets up for future changes. --- packages/swingset-liveslots/src/liveslots.js | 26 +++++++++----- packages/swingset-liveslots/src/stop-vat.js | 1 - .../swingset-liveslots/src/watchedPromises.js | 35 ++++++++++--------- .../tools/fakeVirtualSupport.js | 19 ++++++---- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/packages/swingset-liveslots/src/liveslots.js b/packages/swingset-liveslots/src/liveslots.js index 02e3711a249..e903a3f228a 100644 --- a/packages/swingset-liveslots/src/liveslots.js +++ b/packages/swingset-liveslots/src/liveslots.js @@ -551,7 +551,14 @@ function build( const knownResolutions = new WeakMap(); - // this is called with all outbound argument vrefs + /** + * Determines if a vref from an outbound argument + * identifies a promise that should be exported, and if so then + * adds it to exportedVPIDs and sets up handlers. + * + * @param {any} vref + * @returns {boolean} + */ function maybeExportPromise(vref) { // we only care about new vpids if ( @@ -570,7 +577,9 @@ function build( // if (!knownResolutions.has(p)) { // TODO really? // eslint-disable-next-line no-use-before-define followForKernel(vpid, p); + return true; } + return false; } function exportPassByPresence() { @@ -657,18 +666,15 @@ function build( assertAcceptableSyscallCapdataSize, ); - const watchedPromiseManager = makeWatchedPromiseManager( + const watchedPromiseManager = makeWatchedPromiseManager({ syscall, vrm, vom, collectionManager, // eslint-disable-next-line no-use-before-define convertValToSlot, - unmeteredConvertSlotToVal, - // eslint-disable-next-line no-use-before-define - meterControl.unmetered(revivePromise), - unmeteredUnserialize, - ); + convertSlotToVal: unmeteredConvertSlotToVal, + }); function convertValToSlot(val) { // lsdebug(`serializeToSlot`, val, Object.isFrozen(val)); @@ -816,6 +822,7 @@ function build( registerValue(slot, p); return p; } + const unmeteredRevivePromise = meterControl.unmetered(revivePromise); function resolutionCollector() { const resolutions = []; @@ -1424,7 +1431,10 @@ function build( Fail`buildRootObject() for vat ${forVatID} returned ${rootObject} with no interface`; // Need to load watched promises *after* buildRootObject() so that handler kindIDs // have a chance to be reassociated with their handlers. - watchedPromiseManager.loadWatchedPromiseTable(); + watchedPromiseManager.loadWatchedPromiseTable( + unmeteredUnserialize, + unmeteredRevivePromise, + ); const rootSlot = makeVatSlot('object', true, BigInt(0)); valToSlot.set(rootObject, rootSlot); diff --git a/packages/swingset-liveslots/src/stop-vat.js b/packages/swingset-liveslots/src/stop-vat.js index a522b5db99e..4a5ab226ff5 100644 --- a/packages/swingset-liveslots/src/stop-vat.js +++ b/packages/swingset-liveslots/src/stop-vat.js @@ -279,7 +279,6 @@ function deleteCollectionsWithDecref({ syscall, vrm }) { // END: the preceding functions aren't ready for use yet export async function releaseOldState(tools) { - // The next step is to pretend that the kernel has dropped all // non-durable exports: both the in-RAM Remotables and the on-disk // virtual objects (but not the root object). This will trigger diff --git a/packages/swingset-liveslots/src/watchedPromises.js b/packages/swingset-liveslots/src/watchedPromises.js index 8a7e6cabbc7..2edc26ac540 100644 --- a/packages/swingset-liveslots/src/watchedPromises.js +++ b/packages/swingset-liveslots/src/watchedPromises.js @@ -7,27 +7,23 @@ import { E } from '@endo/eventual-send'; import { parseVatSlot } from './parseVatSlots.js'; /** - * - * @param {*} syscall - * @param {*} vrm - * @param {import('./virtualObjectManager.js').VirtualObjectManager} vom - * @param {*} cm - * @param {*} convertValToSlot - * @param {*} convertSlotToVal - * @param {*} [revivePromise] - * @param {*} [unserialize] + * @param {object} options + * @param {*} options.syscall + * @param {*} options.vrm + * @param {import('./virtualObjectManager.js').VirtualObjectManager} options.vom + * @param {*} options.collectionManager + * @param {import('@endo/marshal').ConvertValToSlot} options.convertValToSlot + * @param {import('@endo/marshal').ConvertSlotToVal} options.convertSlotToVal */ -export function makeWatchedPromiseManager( +export function makeWatchedPromiseManager({ syscall, vrm, vom, - cm, + collectionManager, convertValToSlot, convertSlotToVal, - revivePromise, - unserialize, -) { - const { makeScalarBigMapStore } = cm; +}) { + const { makeScalarBigMapStore } = collectionManager; const { defineDurableKind } = vom; // virtual Store (not durable) mapping vpid to Promise objects, to @@ -104,7 +100,14 @@ export function makeWatchedPromiseManager( ); } - function loadWatchedPromiseTable() { + /** + * Revives watched promises. + * + * @param {import('@endo/marshal').Unserialize} unserialize + * @param {(vref: any) => Promise} revivePromise + * @returns {void} + */ + function loadWatchedPromiseTable(unserialize, revivePromise) { const deadPromisesRaw = syscall.vatstoreGet('deadPromises'); if (!deadPromisesRaw) { return; diff --git a/packages/swingset-liveslots/tools/fakeVirtualSupport.js b/packages/swingset-liveslots/tools/fakeVirtualSupport.js index b8639f998d1..6e90a9e073e 100644 --- a/packages/swingset-liveslots/tools/fakeVirtualSupport.js +++ b/packages/swingset-liveslots/tools/fakeVirtualSupport.js @@ -268,15 +268,20 @@ export function makeFakeVirtualReferenceManager( ); } -export function makeFakeWatchedPromiseManager(vrm, vom, cm, fakeStuff) { - return makeWatchedPromiseManager( - fakeStuff.syscall, +export function makeFakeWatchedPromiseManager( + vrm, + vom, + collectionManager, + fakeStuff, +) { + return makeWatchedPromiseManager({ + syscall: fakeStuff.syscall, vrm, vom, - cm, - fakeStuff.convertValToSlot, - fakeStuff.convertSlotToVal, - ); + collectionManager, + convertValToSlot: fakeStuff.convertValToSlot, + convertSlotToVal: fakeStuff.convertSlotToVal, + }); } /** * Configure virtual stuff with relaxed durability rules and fake liveslots From dd29ff35c5dc72efbbf7087849182aa7f04b2bb1 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 8 Mar 2023 21:11:52 -0500 Subject: [PATCH 13/25] fix(swingset-liveslots): Move promise rejection responsibility into the kernel Fixes #6694 --- packages/SwingSet/src/kernel/kernelQueue.js | 4 +- packages/swingset-liveslots/src/liveslots.js | 24 +------ .../swingset-liveslots/src/watchedPromises.js | 65 ++++--------------- .../tools/fakeVirtualSupport.js | 4 ++ 4 files changed, 21 insertions(+), 76 deletions(-) diff --git a/packages/SwingSet/src/kernel/kernelQueue.js b/packages/SwingSet/src/kernel/kernelQueue.js index 7f3fc29b808..3a8672b74d8 100644 --- a/packages/SwingSet/src/kernel/kernelQueue.js +++ b/packages/SwingSet/src/kernel/kernelQueue.js @@ -45,9 +45,7 @@ export function makeKernelQueueHandler(tools) { const p = kernelKeeper.getResolveablePromise(kpid, vatID); const { subscribers } = p; for (const subscriber of subscribers) { - if (subscriber !== vatID) { - notify(subscriber, kpid); - } + notify(subscriber, kpid); } kernelKeeper.resolveKernelPromise(kpid, rejected, data); const tag = rejected ? 'rejected' : 'fulfilled'; diff --git a/packages/swingset-liveslots/src/liveslots.js b/packages/swingset-liveslots/src/liveslots.js index e903a3f228a..82cf235a2dd 100644 --- a/packages/swingset-liveslots/src/liveslots.js +++ b/packages/swingset-liveslots/src/liveslots.js @@ -552,7 +552,7 @@ function build( const knownResolutions = new WeakMap(); /** - * Determines if a vref from an outbound argument + * Determines if a vref from a watched promise or outbound argument * identifies a promise that should be exported, and if so then * adds it to exportedVPIDs and sets up handlers. * @@ -674,6 +674,7 @@ function build( // eslint-disable-next-line no-use-before-define convertValToSlot, convertSlotToVal: unmeteredConvertSlotToVal, + maybeExportPromise, }); function convertValToSlot(val) { @@ -1190,7 +1191,6 @@ function build( // upgrade, or if we acquire decider authority for a // previously-imported promise if (pRec) { - // TODO: insist that we do not have decider authority for promiseID meterControl.assertNotMetered(); const val = m.unserialize(data); if (rejected) { @@ -1431,10 +1431,7 @@ function build( Fail`buildRootObject() for vat ${forVatID} returned ${rootObject} with no interface`; // Need to load watched promises *after* buildRootObject() so that handler kindIDs // have a chance to be reassociated with their handlers. - watchedPromiseManager.loadWatchedPromiseTable( - unmeteredUnserialize, - unmeteredRevivePromise, - ); + watchedPromiseManager.loadWatchedPromiseTable(unmeteredRevivePromise); const rootSlot = makeVatSlot('object', true, BigInt(0)); valToSlot.set(rootObject, rootSlot); @@ -1526,26 +1523,11 @@ function build( assert(!didStopVat); didStopVat = true; - // all vpids are either "imported" (kernel knows about it and - // kernel decides), "exported" (kernel knows about it but we - // decide), or neither (local, we decide, kernel is unaware). TODO - // this could be cheaper if we tracked all three states (make a - // Set for "neither") instead of doing enumeration and set math. - try { - // mark "imported" plus "neither" for rejection at next startup - const importedVPIDsSet = new Set(importedVPIDs.keys()); - watchedPromiseManager.prepareShutdownRejections( - importedVPIDsSet, - disconnectObjectCapData, - ); - // reject all "exported" vpids now - const deciderVPIDs = Array.from(exportedVPIDs.keys()).sort(); // eslint-disable-next-line @jessie.js/no-nested-await await releaseOldState({ m, disconnectObjectCapData, - deciderVPIDs, syscall, exportedRemotables, addToPossiblyDeadSet, diff --git a/packages/swingset-liveslots/src/watchedPromises.js b/packages/swingset-liveslots/src/watchedPromises.js index 2edc26ac540..2f3ffc1851a 100644 --- a/packages/swingset-liveslots/src/watchedPromises.js +++ b/packages/swingset-liveslots/src/watchedPromises.js @@ -14,6 +14,7 @@ import { parseVatSlot } from './parseVatSlots.js'; * @param {*} options.collectionManager * @param {import('@endo/marshal').ConvertValToSlot} options.convertValToSlot * @param {import('@endo/marshal').ConvertSlotToVal} options.convertSlotToVal + * @param {(vref: any) => boolean} options.maybeExportPromise */ export function makeWatchedPromiseManager({ syscall, @@ -22,6 +23,7 @@ export function makeWatchedPromiseManager({ collectionManager, convertValToSlot, convertSlotToVal, + maybeExportPromise, }) { const { makeScalarBigMapStore } = collectionManager; const { defineDurableKind } = vom; @@ -103,41 +105,14 @@ export function makeWatchedPromiseManager({ /** * Revives watched promises. * - * @param {import('@endo/marshal').Unserialize} unserialize * @param {(vref: any) => Promise} revivePromise * @returns {void} */ - function loadWatchedPromiseTable(unserialize, revivePromise) { - const deadPromisesRaw = syscall.vatstoreGet('deadPromises'); - if (!deadPromisesRaw) { - return; - } - const disconnectObjectCapData = JSON.parse( - syscall.vatstoreGet('deadPromiseDO'), - ); - const disconnectObject = unserialize(disconnectObjectCapData); - syscall.vatstoreDelete('deadPromises'); - syscall.vatstoreDelete('deadPromiseDO'); - const deadPromises = new Set(deadPromisesRaw.split(',')); - - for (const [vpid, watches] of watchedPromiseTable.entries()) { - if (deadPromises.has(vpid)) { - watchedPromiseTable.delete(vpid); - for (const watch of watches) { - const [watcher, ...args] = watch; - void Promise.resolve().then(() => { - if (watcher.onRejected) { - watcher.onRejected(disconnectObject, ...args); - } else { - throw disconnectObject; - } - }); - } - } else { - const p = revivePromise(vpid); - promiseRegistrations.init(vpid, p); - pseudoThen(p, vpid); - } + function loadWatchedPromiseTable(revivePromise) { + for (const vpid of watchedPromiseTable.keys()) { + const p = revivePromise(vpid); + promiseRegistrations.init(vpid, p); + pseudoThen(p, vpid); } } @@ -178,7 +153,6 @@ export function makeWatchedPromiseManager({ // TODO: add vpid->p virtual table mapping, to keep registration alive // TODO: remove mapping upon resolution - // TODO: track watched but non-exported promises, add during prepareShutdownRejections // maybe check importedVPIDs here and add to table if !has void Promise.resolve().then(() => { const watcherVref = convertValToSlot(watcher); @@ -205,35 +179,22 @@ export function makeWatchedPromiseManager({ watchedPromiseTable.set(vpid, harden([...watches, [watcher, ...args]])); } else { watchedPromiseTable.init(vpid, harden([[watcher, ...args]])); + + // Ensure that this vat's promises are rejected at termination. + if (maybeExportPromise(vpid)) { + syscall.subscribe(vpid); + } + promiseRegistrations.init(vpid, p); pseudoThen(p, vpid); } }); } - function prepareShutdownRejections( - importedVPIDsSet, - disconnectObjectCapData, - ) { - const deadPromises = []; - for (const vpid of watchedPromiseTable.keys()) { - if (!importedVPIDsSet.has(vpid)) { - deadPromises.push(vpid); // "exported" plus "neither" vpids - } - } - deadPromises.sort(); // just in case - syscall.vatstoreSet('deadPromises', deadPromises.join(',')); - syscall.vatstoreSet( - 'deadPromiseDO', - JSON.stringify(disconnectObjectCapData), - ); - } - return harden({ preparePromiseWatcherTables, loadWatchedPromiseTable, providePromiseWatcher, watchPromise, - prepareShutdownRejections, }); } diff --git a/packages/swingset-liveslots/tools/fakeVirtualSupport.js b/packages/swingset-liveslots/tools/fakeVirtualSupport.js index 6e90a9e073e..89c91d0b541 100644 --- a/packages/swingset-liveslots/tools/fakeVirtualSupport.js +++ b/packages/swingset-liveslots/tools/fakeVirtualSupport.js @@ -230,6 +230,8 @@ export function makeFakeLiveSlotsStuff(options = {}) { function assertAcceptableSyscallCapdataSize(_capdatas) {} + const maybeExportPromise = _vref => false; + return { syscall, allocateExportID, @@ -250,6 +252,7 @@ export function makeFakeLiveSlotsStuff(options = {}) { dumpStore, setVrm, assertAcceptableSyscallCapdataSize, + maybeExportPromise, }; } @@ -281,6 +284,7 @@ export function makeFakeWatchedPromiseManager( collectionManager, convertValToSlot: fakeStuff.convertValToSlot, convertSlotToVal: fakeStuff.convertSlotToVal, + maybeExportPromise: fakeStuff.maybeExportPromise, }); } /** From cf2c34df306c47500b4abcccc5d6f7dfff5f47b2 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 8 Mar 2023 21:21:20 -0500 Subject: [PATCH 14/25] docs(swingset-liveslots): Update VPID management documentation for code changes --- packages/swingset-liveslots/src/vpid-tracking.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swingset-liveslots/src/vpid-tracking.md b/packages/swingset-liveslots/src/vpid-tracking.md index e8725c5360d..dec609b7af6 100644 --- a/packages/swingset-liveslots/src/vpid-tracking.md +++ b/packages/swingset-liveslots/src/vpid-tracking.md @@ -13,7 +13,7 @@ There are roughly four ways that liveslots might become aware of a promise: * deserialization: the arguments of an inbound `dispatch.deliver` or `dispatch.notify` are deserialized, and a new Promise instance is created * inbound result: the kernel-allocated `result` VPID of an inbound `dispatch.deliver` is associated with the Promise we get back from `HandledPromise.applyMethod` -A Promise may be associated with a VPID even though the kernel does not know about it (i.e. the VPID is not in the kernel's c-list for that vat). This can occur when a Promise is stored into virtual data without also being sent to (or received from) the kernel. It can also occur when a Promise is resolved but still referenced in vdata (the kernel's knowledge is temporary; it retires VPIDs from c-lists upon `syscall.resolve` or `dispatch.notify` as relevant). So a VPID might start out stored only in vdata, then get sent to the kernel, then get resolved, leaving it solely in vdata once more. +A Promise may be associated with a VPID even though the kernel does not know about it (i.e. the VPID is not in the kernel's c-list for that vat). This can occur when a Promise is stored into virtual data without also being sent to (or received from) the kernel, although note that every Promise associated with a durable promise watcher _is_ sent to the kernel so it can be rejected during vat upgrade. A Promise can also be resolved but still referenced in vdata and forgotten by the kernel (the kernel's knowledge is temporary; it retires VPIDs from c-lists upon `syscall.resolve` or `dispatch.notify` as relevant). So a VPID might start out stored only in vdata, then get sent to the kernel, then get resolved, leaving it solely in vdata once more. Each unresolved VPID has a decider: either the kernel or a vat. It can remain unresolved for arbitrarily long, but becomes resolved by the first of the following events: From 01729ab55648fc0a8b4bd42cb444ec42aa054121 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 8 Mar 2023 21:21:20 -0500 Subject: [PATCH 15/25] docs(swingset-liveslots): Clarify VPID managment documentation --- .../swingset-liveslots/src/vpid-tracking.md | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/swingset-liveslots/src/vpid-tracking.md b/packages/swingset-liveslots/src/vpid-tracking.md index dec609b7af6..80ada85849f 100644 --- a/packages/swingset-liveslots/src/vpid-tracking.md +++ b/packages/swingset-liveslots/src/vpid-tracking.md @@ -2,9 +2,9 @@ Kernels and vats communicate about promises by referring to their VPIDs: vat(-centric) promise IDs. These are strings like `p+12` and `p-23`. Like VOIDs (object IDs), the plus/minus sign indicates which side of the boundary allocated the number (`p+12` and `o+12` are allocated by the vat, `p-13` and `o-13` are allocated by the kernel). But where the object ID sign also indicates which side "owns" the object (i.e. where the behavior lives), the promise ID sign is generally irrelevant. -Instead, we care about which side holds the resolution authority for a promise (referred to as being its **decider**). This is not indicated by the VPID sign, and in fact is not necessarily static. Liveslots does not currently have any mechanism to allow one promise to be forwarded to another, but if it acquires this some day, then the decider of a promise could shift from kernel to vat to kernel again before it finally gets resolved. And there *are* sequences that allow a vat to receive a reference to a promise in method arguments before receiving a message for which that same VPID identifies the result promise (e.g., `pk = makePromiseKit(); p2 = E(pk.promise).push('queued'); await E(observer).push(p2); pk.resolve(observer)` with an observer whose `push` returns a prompt response). In such cases, the decider is initially the kernel but later becomes the receiving vat. +Instead, we care about which side holds the resolution authority for a promise (referred to as being its **decider**). This is not indicated by the VPID sign, and in fact is not necessarily static. Liveslots does not currently have any mechanism to allow one promise to be forwarded to another, but if it acquires this some day, then the decider of a promise could shift from kernel to vat to kernel again before it finally gets resolved. And there *are* sequences that allow a vat to receive a reference to a promise in method arguments before receiving a message for which that same VPID identifies the result promise (e.g., `{ promise: p1, resolve: resolveP1 } = makePromiseKit(); p2 = E(p1).push('queued'); await E(observer).push(p2); resolveP1(observer);` with an observer whose `push` returns a prompt response). In such cases, the decider is initially the kernel but later becomes the receiving vat. -Each Promise starts out in the "unresolved" state, then later transitions irrevocably to the "resolved" state. Liveslots frequently (but not always) pays attention to this transition by calling `then` to attach fulfillment/rejection callbacks. To handle resolution cycles, liveslots remembers the resolution of old promises in a `WeakMap` for as long as the Promise exists. Consequently, for liveslots' purposes, every Promise is either resolved (a callback has fired and liveslots remembers the settlement), or unresolved (liveslots has not yet seen a resolution). +Each Promise starts out in the "unresolved" state, then later transitions irrevocably to the "resolved" state. Liveslots frequently (but not always) pays attention to this transition by calling `then` to attach fulfillment/rejection settlement callbacks. To handle resolution cycles, liveslots remembers the resolution of old promises in a `WeakMap` for as long as the Promise exists. Consequently, for liveslots' purposes, every Promise is either resolved (a callback has fired and liveslots remembers the settlement), or unresolved (liveslots has not yet seen a resolution that settles it). There are roughly four ways that liveslots might become aware of a promise: @@ -13,13 +13,14 @@ There are roughly four ways that liveslots might become aware of a promise: * deserialization: the arguments of an inbound `dispatch.deliver` or `dispatch.notify` are deserialized, and a new Promise instance is created * inbound result: the kernel-allocated `result` VPID of an inbound `dispatch.deliver` is associated with the Promise we get back from `HandledPromise.applyMethod` -A Promise may be associated with a VPID even though the kernel does not know about it (i.e. the VPID is not in the kernel's c-list for that vat). This can occur when a Promise is stored into virtual data without also being sent to (or received from) the kernel, although note that every Promise associated with a durable promise watcher _is_ sent to the kernel so it can be rejected during vat upgrade. A Promise can also be resolved but still referenced in vdata and forgotten by the kernel (the kernel's knowledge is temporary; it retires VPIDs from c-lists upon `syscall.resolve` or `dispatch.notify` as relevant). So a VPID might start out stored only in vdata, then get sent to the kernel, then get resolved, leaving it solely in vdata once more. +A Promise may be associated with a VPID even though the kernel does not know about it (i.e. the VPID is not in the kernel's c-list for that vat). This can occur when a Promise is stored into virtual data without also being sent to (or received from) the kernel, although note that every Promise associated with a durable promise watcher _is_ sent to the kernel so it can be rejected during vat upgrade. A Promise can also be resolved but still referenced in vdata and forgotten by the kernel (the kernel's knowledge is temporary; it retires VPIDs from c-lists upon `syscall.resolve` or `dispatch.notify` as appropriate). So a VPID might start out stored only in vdata, then get sent to the kernel, then get resolved, leaving it solely in vdata once more. Each unresolved VPID has a decider: either the kernel or a vat. It can remain unresolved for arbitrarily long, but becomes resolved by the first of the following events: -* if userspace resolves the corresponding Promise and liveslots learns of the resolution, then liveslots will perform a `syscall.resolve()` -* if the vat is upgraded, liveslots will perform a `syscall.resolve()` of all remaining VPIDs during the delivery of `dispatch.stopVat()` (after which all Promises evaporate along with the rest of the JS heap) +* if liveslots learns about local resolution of the corresponding Promise by userspace, then liveslots will perform a `syscall.resolve()` (prompting 'notify' deliveries to other subscribed vats) +* if liveslots learns about resolution by inbound notify, then liveslots will unregister it as necessary and inform userspace of the resolution * if the vat is terminated, the kernel internally rejects all remaining vat-decided KPIDs without involving the vat +* if the vat is upgraded, each of those terminate-associated rejections is followed by a 'notify' delivery to the new incarnation Liveslots tracks promises in the following data structures: @@ -54,7 +55,7 @@ Remember that the `slotToVal` registration uses a WeakRef, so being registered t * at this point, there is not yet a strong reference to the Promise * When a VPID appears in the serialized arguments of `syscall.send` or `syscall.resolve`: * if the VPID already exists in `exportedVPIDs` or `importedVPIDs`: do nothing - * else: use `followForKernel` to add the VPID to `exportedVPIDs` and attach settlement callbacks + * else: use `followForKernel` to add the VPID to `exportedVPIDs` and attach `.then(onFulfill, onReject)` callbacks that will map fulfillment/rejection to `syscall.resolve()` * When a `followForKernel` settlement callback is executed: * do `syscall.resolve()` * remove from `exportedVPIDs` @@ -77,10 +78,11 @@ Remember that the `slotToVal` registration uses a WeakRef, so being registered t * add the Promise and its `resolve`/`reject` pair to `importedVPIDs` * register the Promise in `valToSlot`/`slotToVal` * use `syscall.subscribe` to request a `dispatch.notify` delivery when the kernel resolves this promise -* When a VPID appears as the `result` of an inbound `dispatch.deliver`: - * if the VPID is present in `importedVPIDs`: retrieve the `[resolve, reject]` pRec and use `resolve(res)` to forward the invocation result promise to the previously-imported promise, then remove the VPID from `importedVPIDs` - * else: register `res` the invocation result promise under the VPID - * in either case, use `followForKernel` to add the VPID to `exportedVPIDs` and attach settlement callbacks +* When a VPID appears as the `result` of an inbound `dispatch.deliver`, the vat is responsible for deciding it: + * construct a promise `res` to capture the userspace-provided result + * if the VPID is present in `importedVPIDs`: retrieve the `[resolve, reject]` pRec and use `resolve(res)` to forward eventual settlement of `res` to settlement of the previously-imported promise, then remove the VPID from `importedVPIDs` + * else: register marshaller association between the VPID and `res` + * in either case, use `followForKernel` to add the VPID to `exportedVPIDs` and attach `.then(onFulfill, onReject)` callbacks that will map fulfillment/rejection to `syscall.resolve() If the serialization is for storage in virtual data, the act of storing the VPID will add the Promise to `remotableRefCounts`, which maintains a strong reference for as long as the VPID is held. When it is removed from virtual data (or the object/collection is deleted), the refcount will be decremented. When the refcount drops to zero, we perform the `exportedVPIDs`/`importedVPIDs` check and then maybe unregister the promise. From be8adc3d92b8fd8177ac150fb69dc4d6206fbfd6 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 13 Mar 2023 00:23:20 -0400 Subject: [PATCH 16/25] test: Move full-environment test from liveslots to SwingSet --- packages/SwingSet/test/test-promises.js | 71 +++++++++++++++++- .../test/vat-durable-promise-watcher.js | 0 packages/swingset-liveslots/package.json | 4 +- .../test/test-vat-upgrade.js | 73 ------------------- 4 files changed, 71 insertions(+), 77 deletions(-) rename packages/{swingset-liveslots => SwingSet}/test/vat-durable-promise-watcher.js (100%) delete mode 100644 packages/swingset-liveslots/test/test-vat-upgrade.js diff --git a/packages/SwingSet/test/test-promises.js b/packages/SwingSet/test/test-promises.js index 272aeea929b..8ed2724f390 100644 --- a/packages/SwingSet/test/test-promises.js +++ b/packages/SwingSet/test/test-promises.js @@ -6,7 +6,9 @@ import { loadBasedir, buildKernelBundles, } from '../src/index.js'; -import { kser, kslot } from '../src/lib/kmarshal.js'; +import { kser, kslot, kunser } from '../src/lib/kmarshal.js'; + +const bfile = name => new URL(name, import.meta.url).pathname; test.before(async t => { const kernelBundles = await buildKernelBundles(); @@ -212,3 +214,70 @@ test('refcount while queued', async t => { await c.run(); t.deepEqual(c.kpResolution(kpid4), kser([true, 3])); }); + +test('local promises are rejected by vat upgrade', async t => { + // TODO: Generalize packages/SwingSet/test/upgrade/test-upgrade.js + /** @type {SwingSetConfig} */ + const config = { + includeDevDependencies: true, // for vat-data + defaultManagerType: 'xs-worker', + bootstrap: 'bootstrap', + defaultReapInterval: 'never', + vats: { + bootstrap: { + sourceSpec: bfile('./bootstrap-relay.js'), + }, + }, + bundles: { + watcher: { sourceSpec: bfile('./vat-durable-promise-watcher.js') }, + }, + }; + const c = await buildVatController(config); + t.teardown(c.shutdown); + c.pinVatRoot('bootstrap'); + await c.run(); + + const run = async (method, args = []) => { + assert(Array.isArray(args)); + const kpid = c.queueToVatRoot('bootstrap', method, args); + await c.run(); + const status = c.kpStatus(kpid); + if (status === 'fulfilled') { + const result = c.kpResolution(kpid); + return kunser(result); + } + assert(status === 'rejected'); + const err = c.kpResolution(kpid); + throw kunser(err); + }; + const messageVat = (name, methodName, args) => + run('messageVat', [{ name, methodName, args }]); + // eslint-disable-next-line no-underscore-dangle + const _messageObject = (presence, methodName, args) => + run('messageVatObject', [{ presence, methodName, args }]); + + const S = Symbol.for('passable'); + await run('createVat', [{ name: 'watcher', bundleCapName: 'watcher' }]); + await messageVat('watcher', 'watchLocalPromise', ['orphaned']); + await messageVat('watcher', 'watchLocalPromise', ['fulfilled', S]); + await messageVat('watcher', 'watchLocalPromise', ['rejected', undefined, S]); + const v1Settlements = await messageVat('watcher', 'getSettlements'); + t.deepEqual(v1Settlements, { + fulfilled: { status: 'fulfilled', value: S }, + rejected: { status: 'rejected', reason: S }, + }); + await run('upgradeVat', [{ name: 'watcher', bundleCapName: 'watcher' }]); + const v2Settlements = await messageVat('watcher', 'getSettlements'); + t.deepEqual(v2Settlements, { + fulfilled: { status: 'fulfilled', value: S }, + rejected: { status: 'rejected', reason: S }, + orphaned: { + status: 'rejected', + reason: { + name: 'vatUpgraded', + upgradeMessage: 'vat upgraded', + incarnationNumber: 1, + }, + }, + }); +}); diff --git a/packages/swingset-liveslots/test/vat-durable-promise-watcher.js b/packages/SwingSet/test/vat-durable-promise-watcher.js similarity index 100% rename from packages/swingset-liveslots/test/vat-durable-promise-watcher.js rename to packages/SwingSet/test/vat-durable-promise-watcher.js diff --git a/packages/swingset-liveslots/package.json b/packages/swingset-liveslots/package.json index 0fa13f591e0..9efed053c92 100644 --- a/packages/swingset-liveslots/package.json +++ b/packages/swingset-liveslots/package.json @@ -16,9 +16,7 @@ "lint:types": "tsc -p jsconfig.json", "lint:eslint": "eslint ." }, - "devDependencies": { - "@agoric/swingset-vat": "^0.30.2" - }, + "devDependencies": {}, "dependencies": { "@agoric/assert": "^0.5.1", "@agoric/internal": "^0.2.1", diff --git a/packages/swingset-liveslots/test/test-vat-upgrade.js b/packages/swingset-liveslots/test/test-vat-upgrade.js deleted file mode 100644 index d03a2ca3411..00000000000 --- a/packages/swingset-liveslots/test/test-vat-upgrade.js +++ /dev/null @@ -1,73 +0,0 @@ -import '@agoric/swingset-vat/tools/prepare-test-env.js'; -import test from 'ava'; -import { buildVatController } from '@agoric/swingset-vat'; -import { kunser } from '@agoric/swingset-vat/src/lib/kmarshal.js'; - -const bfile = name => new URL(name, import.meta.url).pathname; - -test('local promises are rejected by vat upgrade', async t => { - // TODO: Generalize packages/SwingSet/test/upgrade/test-upgrade.js - /** @type {SwingSetConfig} */ - const config = { - includeDevDependencies: true, // for vat-data - defaultManagerType: 'xs-worker', - bootstrap: 'bootstrap', - defaultReapInterval: 'never', - vats: { - bootstrap: { - sourceSpec: bfile('../../SwingSet/test/bootstrap-relay.js'), - }, - }, - bundles: { - watcher: { sourceSpec: bfile('./vat-durable-promise-watcher.js') }, - }, - }; - const c = await buildVatController(config); - t.teardown(c.shutdown); - c.pinVatRoot('bootstrap'); - await c.run(); - - const run = async (method, args = []) => { - assert(Array.isArray(args)); - const kpid = c.queueToVatRoot('bootstrap', method, args); - await c.run(); - const status = c.kpStatus(kpid); - if (status === 'fulfilled') { - const result = c.kpResolution(kpid); - return kunser(result); - } - assert(status === 'rejected'); - const err = c.kpResolution(kpid); - throw kunser(err); - }; - const messageVat = (name, methodName, args) => - run('messageVat', [{ name, methodName, args }]); - // eslint-disable-next-line no-underscore-dangle - const _messageObject = (presence, methodName, args) => - run('messageVatObject', [{ presence, methodName, args }]); - - const S = Symbol.for('passable'); - await run('createVat', [{ name: 'watcher', bundleCapName: 'watcher' }]); - await messageVat('watcher', 'watchLocalPromise', ['orphaned']); - await messageVat('watcher', 'watchLocalPromise', ['fulfilled', S]); - await messageVat('watcher', 'watchLocalPromise', ['rejected', undefined, S]); - const v1Settlements = await messageVat('watcher', 'getSettlements'); - t.deepEqual(v1Settlements, { - fulfilled: { status: 'fulfilled', value: S }, - rejected: { status: 'rejected', reason: S }, - }); - await run('upgradeVat', [{ name: 'watcher', bundleCapName: 'watcher' }]); - const v2Settlements = await messageVat('watcher', 'getSettlements'); - t.deepEqual(v2Settlements, { - fulfilled: { status: 'fulfilled', value: S }, - rejected: { status: 'rejected', reason: S }, - orphaned: { - status: 'rejected', - reason: { - name: 'vatUpgraded', - upgradeMessage: 'vat upgraded', - incarnationNumber: 1, - }, - }, - }); -}); From abbf2a87f60cee3c239b0da5163f9a301179dc9b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 13 Mar 2023 11:31:17 -0400 Subject: [PATCH 17/25] refactor(vat-data): Merge kind-utils.js into exo-utils.js --- packages/vat-data/src/exo-utils.js | 61 ++++++++++++++++++- packages/vat-data/src/index.js | 8 ++- packages/vat-data/src/kind-utils.js | 68 ---------------------- packages/vat-data/src/vat-data-bindings.js | 2 +- 4 files changed, 66 insertions(+), 73 deletions(-) delete mode 100644 packages/vat-data/src/kind-utils.js diff --git a/packages/vat-data/src/exo-utils.js b/packages/vat-data/src/exo-utils.js index e84cb504e48..7a792fad623 100644 --- a/packages/vat-data/src/exo-utils.js +++ b/packages/vat-data/src/exo-utils.js @@ -1,11 +1,11 @@ import { initEmpty } from '@agoric/store'; -import { provideKindHandle } from './kind-utils.js'; import { defineKind, defineKindMulti, defineDurableKind, defineDurableKindMulti, + makeKindHandle, provide, } from './vat-data-bindings.js'; @@ -17,6 +17,65 @@ import { /** @template T @typedef {import('./types.js').KindFacets} KindFacets */ /** @typedef {import('./types.js').DurableKindHandle} DurableKindHandle */ +/** + * Make a version of the argument function that takes a kind context but + * ignores it. + * + * @type {(fn: T) => import('./types.js').PlusContext} + */ +export const ignoreContext = + fn => + (_context, ...args) => + fn(...args); +harden(ignoreContext); + +/** + * @param {Baggage} baggage + * @param {string} kindName + * @returns {DurableKindHandle} + */ +export const provideKindHandle = (baggage, kindName) => + provide(baggage, `${kindName}_kindHandle`, () => makeKindHandle(kindName)); +harden(provideKindHandle); + +/** + * @deprecated Use prepareExoClass instead + * @type {import('./types.js').PrepareKind} + */ +export const prepareKind = ( + baggage, + kindName, + init, + behavior, + options = undefined, +) => + defineDurableKind( + provideKindHandle(baggage, kindName), + init, + behavior, + options, + ); +harden(prepareKind); + +/** + * @deprecated Use prepareExoClassKit instead + * @type {import('./types.js').PrepareKindMulti} + */ +export const prepareKindMulti = ( + baggage, + kindName, + init, + behavior, + options = undefined, +) => + defineDurableKindMulti( + provideKindHandle(baggage, kindName), + init, + behavior, + options, + ); +harden(prepareKindMulti); + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 /** * @template {(...args: any) => any} I init state function diff --git a/packages/vat-data/src/index.js b/packages/vat-data/src/index.js index 4bff2886698..cb0e4076ed7 100644 --- a/packages/vat-data/src/index.js +++ b/packages/vat-data/src/index.js @@ -35,7 +35,7 @@ export { prepareExoClass, prepareExoClassKit, prepareExo, - // deorecated + // deprecated prepareSingleton, } from './exo-utils.js'; @@ -45,10 +45,12 @@ export { // //////////////////////////// deprecated ///////////////////////////////////// +/** + * @deprecated Use Exos/ExoClasses instead of Kinds + */ export { - // deprecated ignoreContext, provideKindHandle, prepareKind, prepareKindMulti, -} from './kind-utils.js'; +} from './exo-utils.js'; diff --git a/packages/vat-data/src/kind-utils.js b/packages/vat-data/src/kind-utils.js deleted file mode 100644 index 1084f4dfdf0..00000000000 --- a/packages/vat-data/src/kind-utils.js +++ /dev/null @@ -1,68 +0,0 @@ -import { - provide, - defineDurableKind, - defineDurableKindMulti, - makeKindHandle, -} from './vat-data-bindings.js'; - -/** @typedef {import('./types.js').Baggage} Baggage */ -/** @typedef {import('./types.js').DurableKindHandle} DurableKindHandle */ - -/** - * Make a version of the argument function that takes a kind context but - * ignores it. - * - * @type {(fn: T) => import('./types.js').PlusContext} - */ -export const ignoreContext = - fn => - (_context, ...args) => - fn(...args); -harden(ignoreContext); - -/** - * @param {Baggage} baggage - * @param {string} kindName - * @returns {DurableKindHandle} - */ -export const provideKindHandle = (baggage, kindName) => - provide(baggage, `${kindName}_kindHandle`, () => makeKindHandle(kindName)); -harden(provideKindHandle); - -/** - * @deprecated Use prepareExoClass instead - * @type {import('./types.js').PrepareKind} - */ -export const prepareKind = ( - baggage, - kindName, - init, - behavior, - options = undefined, -) => - defineDurableKind( - provideKindHandle(baggage, kindName), - init, - behavior, - options, - ); -harden(prepareKind); - -/** - * @deprecated Use prepareExoClassKit instead - * @type {import('./types.js').PrepareKindMulti} - */ -export const prepareKindMulti = ( - baggage, - kindName, - init, - behavior, - options = undefined, -) => - defineDurableKindMulti( - provideKindHandle(baggage, kindName), - init, - behavior, - options, - ); -harden(prepareKindMulti); diff --git a/packages/vat-data/src/vat-data-bindings.js b/packages/vat-data/src/vat-data-bindings.js index a20c005dda0..7cacca40144 100644 --- a/packages/vat-data/src/vat-data-bindings.js +++ b/packages/vat-data/src/vat-data-bindings.js @@ -30,7 +30,7 @@ if ('VatData' in globalThis) { } /** - * @deprecated Use Exos/ExoClasses instead of kinds + * @deprecated Use Exos/ExoClasses instead of Kinds */ export const { defineKind, From 835b5e5eadf6db060b2edc202cc237f2c5d70b64 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 13 Mar 2023 20:51:30 -0400 Subject: [PATCH 18/25] refactor(vat-data): Generalize store/kind/exo maker factories to VatData scope --- packages/vat-data/src/exo-utils.js | 500 +++++++++++---------- packages/vat-data/src/vat-data-bindings.js | 88 ++-- 2 files changed, 318 insertions(+), 270 deletions(-) diff --git a/packages/vat-data/src/exo-utils.js b/packages/vat-data/src/exo-utils.js index 7a792fad623..cec5ca1473a 100644 --- a/packages/vat-data/src/exo-utils.js +++ b/packages/vat-data/src/exo-utils.js @@ -1,13 +1,6 @@ import { initEmpty } from '@agoric/store'; -import { - defineKind, - defineKindMulti, - defineDurableKind, - defineDurableKindMulti, - makeKindHandle, - provide, -} from './vat-data-bindings.js'; +import { provide, VatData as globalVatData } from './vat-data-bindings.js'; /** @template L,R @typedef {import('@endo/eventual-send').RemotableBrand} RemotableBrand */ /** @template T @typedef {import('@endo/far').ERef} ERef */ @@ -29,264 +22,291 @@ export const ignoreContext = fn(...args); harden(ignoreContext); -/** - * @param {Baggage} baggage - * @param {string} kindName - * @returns {DurableKindHandle} - */ -export const provideKindHandle = (baggage, kindName) => - provide(baggage, `${kindName}_kindHandle`, () => makeKindHandle(kindName)); -harden(provideKindHandle); +// TODO: Find a good home for this function used by @agoric/vat-data and testing code +export const makeExoUtils = VatData => { + const { + defineKind, + defineKindMulti, + defineDurableKind, + defineDurableKindMulti, + makeKindHandle, + } = VatData; -/** - * @deprecated Use prepareExoClass instead - * @type {import('./types.js').PrepareKind} - */ -export const prepareKind = ( - baggage, - kindName, - init, - behavior, - options = undefined, -) => - defineDurableKind( - provideKindHandle(baggage, kindName), + /** + * @param {Baggage} baggage + * @param {string} kindName + * @returns {DurableKindHandle} + */ + const provideKindHandle = (baggage, kindName) => + provide(baggage, `${kindName}_kindHandle`, () => makeKindHandle(kindName)); + harden(provideKindHandle); + + /** + * @deprecated Use prepareExoClass instead + * @type {import('./types.js').PrepareKind} + */ + const prepareKind = ( + baggage, + kindName, init, behavior, - options, - ); -harden(prepareKind); + options = undefined, + ) => + defineDurableKind( + provideKindHandle(baggage, kindName), + init, + behavior, + options, + ); + harden(prepareKind); -/** - * @deprecated Use prepareExoClassKit instead - * @type {import('./types.js').PrepareKindMulti} - */ -export const prepareKindMulti = ( - baggage, - kindName, - init, - behavior, - options = undefined, -) => - defineDurableKindMulti( - provideKindHandle(baggage, kindName), + /** + * @deprecated Use prepareExoClassKit instead + * @type {import('./types.js').PrepareKindMulti} + */ + const prepareKindMulti = ( + baggage, + kindName, init, behavior, - options, - ); -harden(prepareKindMulti); + options = undefined, + ) => + defineDurableKindMulti( + provideKindHandle(baggage, kindName), + init, + behavior, + options, + ); + harden(prepareKindMulti); -// TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 -/** - * @template {(...args: any) => any} I init state function - * @template T behavior - * @param {string} tag - * @param {any} interfaceGuard - * @param {I} init - * @param {T & ThisType<{ self: T, state: ReturnType }>} methods - * @param {DefineKindOptions<{ self: T, state: ReturnType }>} [options] - * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} - */ -export const defineVirtualExoClass = ( - tag, - interfaceGuard, - init, - methods, - options, -) => - // @ts-expect-error The use of `thisfulMethods` to change - // the appropriate static type is the whole point of this method. - defineKind(tag, init, methods, { - ...options, - thisfulMethods: true, - interfaceGuard, - }); -harden(defineVirtualExoClass); + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 + /** + * @template {(...args: any) => any} I init state function + * @template T behavior + * @param {string} tag + * @param {any} interfaceGuard + * @param {I} init + * @param {T & ThisType<{ self: T, state: ReturnType }>} methods + * @param {DefineKindOptions<{ self: T, state: ReturnType }>} [options] + * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} + */ + const defineVirtualExoClass = (tag, interfaceGuard, init, methods, options) => + defineKind(tag, init, methods, { + ...options, + thisfulMethods: true, + interfaceGuard, + }); + harden(defineVirtualExoClass); -// TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 -/** - * @template {(...args: any) => any} I init state function - * @template {Record>} T facets - * @param {string} tag - * @param {any} interfaceGuardKit - * @param {I} init - * @param {T & ThisType<{ facets: T, state: ReturnType }> } facets - * @param {DefineKindOptions<{ facets: T, state: ReturnType }>} [options] - * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} - */ -export const defineVirtualExoClassKit = ( - tag, - interfaceGuardKit, - init, - facets, - options, -) => - // @ts-expect-error The use of `thisfulMethods` to change - // the appropriate static type is the whole point of this method. - defineKindMulti(tag, init, facets, { - ...options, - thisfulMethods: true, - interfaceGuard: interfaceGuardKit, - }); -harden(defineVirtualExoClassKit); + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 + /** + * @template {(...args: any) => any} I init state function + * @template {Record>} T facets + * @param {string} tag + * @param {any} interfaceGuardKit + * @param {I} init + * @param {T & ThisType<{ facets: T, state: ReturnType }> } facets + * @param {DefineKindOptions<{ facets: T, state: ReturnType }>} [options] + * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} + */ + const defineVirtualExoClassKit = ( + tag, + interfaceGuardKit, + init, + facets, + options, + ) => + defineKindMulti(tag, init, facets, { + ...options, + thisfulMethods: true, + interfaceGuard: interfaceGuardKit, + }); + harden(defineVirtualExoClassKit); -// TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 -/** - * @template {(...args: any) => any} I init state function - * @template {Record} T methods - * @param {DurableKindHandle} kindHandle - * @param {any} interfaceGuard - * @param {I} init - * @param {T & ThisType<{ self: T, state: ReturnType }>} methods - * @param {DefineKindOptions<{ self: T, state: ReturnType }>} [options] - * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} - */ -export const defineDurableExoClass = ( - kindHandle, - interfaceGuard, - init, - methods, - options, -) => - // @ts-expect-error The use of `thisfulMethods` to change - // the appropriate static type is the whole point of this method. - defineDurableKind(kindHandle, init, methods, { - ...options, - thisfulMethods: true, + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 + /** + * @template {(...args: any) => any} I init state function + * @template {Record} T methods + * @param {DurableKindHandle} kindHandle + * @param {any} interfaceGuard + * @param {I} init + * @param {T & ThisType<{ self: T, state: ReturnType }>} methods + * @param {DefineKindOptions<{ self: T, state: ReturnType }>} [options] + * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} + */ + const defineDurableExoClass = ( + kindHandle, interfaceGuard, - }); -harden(defineDurableExoClass); + init, + methods, + options, + ) => + defineDurableKind(kindHandle, init, methods, { + ...options, + thisfulMethods: true, + interfaceGuard, + }); + harden(defineDurableExoClass); -// TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 -/** - * @template {(...args: any) => any} I init state function - * @template {Record>} T facets - * @param {DurableKindHandle} kindHandle - * @param {any} interfaceGuardKit - * @param {I} init - * @param {T & ThisType<{ facets: T, state: ReturnType}> } facets - * @param {DefineKindOptions<{ facets: T, state: ReturnType}>} [options] - * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} - */ -export const defineDurableExoClassKit = ( - kindHandle, - interfaceGuardKit, - init, - facets, - options, -) => - // @ts-expect-error The use of `thisfulMethods` to change - // the appropriate static type is the whole point of this method. - defineDurableKindMulti(kindHandle, init, facets, { - ...options, - thisfulMethods: true, - interfaceGuard: interfaceGuardKit, - }); -harden(defineDurableExoClassKit); + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 + /** + * @template {(...args: any) => any} I init state function + * @template {Record>} T facets + * @param {DurableKindHandle} kindHandle + * @param {any} interfaceGuardKit + * @param {I} init + * @param {T & ThisType<{ facets: T, state: ReturnType}> } facets + * @param {DefineKindOptions<{ facets: T, state: ReturnType}>} [options] + * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} + */ + const defineDurableExoClassKit = ( + kindHandle, + interfaceGuardKit, + init, + facets, + options, + ) => + defineDurableKindMulti(kindHandle, init, facets, { + ...options, + thisfulMethods: true, + interfaceGuard: interfaceGuardKit, + }); + harden(defineDurableExoClassKit); -// TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 -/** - * @template {(...args: any) => any} I init state function - * @template {Record} T methods - * @param {Baggage} baggage - * @param {string} kindName - * @param {any} interfaceGuard - * @param {I} init - * @param {T & ThisType<{ self: T, state: ReturnType }>} methods - * @param {DefineKindOptions<{ self: T, state: ReturnType }>} [options] - * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} - */ -export const prepareExoClass = ( - baggage, - kindName, - interfaceGuard, - init, - methods, - options = undefined, -) => - defineDurableExoClass( - provideKindHandle(baggage, kindName), + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 + /** + * @template {(...args: any) => any} I init state function + * @template {Record} T methods + * @param {Baggage} baggage + * @param {string} kindName + * @param {any} interfaceGuard + * @param {I} init + * @param {T & ThisType<{ self: T, state: ReturnType }>} methods + * @param {DefineKindOptions<{ self: T, state: ReturnType }>} [options] + * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} + */ + const prepareExoClass = ( + baggage, + kindName, interfaceGuard, init, methods, - options, - ); -harden(prepareExoClass); + options = undefined, + ) => + defineDurableExoClass( + provideKindHandle(baggage, kindName), + interfaceGuard, + init, + methods, + options, + ); + harden(prepareExoClass); -// TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 -/** - * @template {(...args: any) => any} I init state function - * @template {Record>} T facets - * @param {Baggage} baggage - * @param {string} kindName - * @param {any} interfaceGuardKit - * @param {I} init - * @param {T & ThisType<{ facets: T, state: ReturnType }> } facets - * @param {DefineKindOptions<{ facets: T, state: ReturnType }>} [options] - * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} - */ -export const prepareExoClassKit = ( - baggage, - kindName, - interfaceGuardKit, - init, - facets, - options = undefined, -) => - defineDurableExoClassKit( - provideKindHandle(baggage, kindName), + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 + /** + * @template {(...args: any) => any} I init state function + * @template {Record>} T facets + * @param {Baggage} baggage + * @param {string} kindName + * @param {any} interfaceGuardKit + * @param {I} init + * @param {T & ThisType<{ facets: T, state: ReturnType }> } facets + * @param {DefineKindOptions<{ facets: T, state: ReturnType }>} [options] + * @returns {(...args: Parameters) => (T & RemotableBrand<{}, T>)} + */ + const prepareExoClassKit = ( + baggage, + kindName, interfaceGuardKit, init, facets, - options, - ); -harden(prepareExoClassKit); + options = undefined, + ) => + defineDurableExoClassKit( + provideKindHandle(baggage, kindName), + interfaceGuardKit, + init, + facets, + options, + ); + harden(prepareExoClassKit); -// TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 -/** - * @template {Record} M methods - * @param {Baggage} baggage - * @param {string} kindName - * @param {any} interfaceGuard - * @param {M} methods - * @param {DefineKindOptions<{ self: M }>} [options] - * @returns {M & RemotableBrand<{}, M>} - */ -export const prepareExo = ( - baggage, - kindName, - interfaceGuard, - methods, - options = undefined, -) => { - const makeSingleton = prepareExoClass( + // TODO interfaceGuard type https://github.com/Agoric/agoric-sdk/issues/6206 + /** + * @template {Record} M methods + * @param {Baggage} baggage + * @param {string} kindName + * @param {any} interfaceGuard + * @param {M} methods + * @param {DefineKindOptions<{ self: M }>} [options] + * @returns {M & RemotableBrand<{}, M>} + */ + const prepareExo = ( baggage, kindName, interfaceGuard, - initEmpty, methods, - options, - ); + options = undefined, + ) => { + const makeSingleton = prepareExoClass( + baggage, + kindName, + interfaceGuard, + initEmpty, + methods, + options, + ); + + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error -- https://github.com/Agoric/agoric-sdk/issues/4620 + // @ts-ignore could be instantiated with an arbitrary type + return provide(baggage, `the_${kindName}`, () => makeSingleton()); + }; + harden(prepareExo); + + /** + * @template {Record} M methods + * @deprecated Use prepareExo instead. + * @param {Baggage} baggage + * @param {string} kindName + * @param {M} methods + * @param {DefineKindOptions<{ self: M }>} [options] + * @returns {M & RemotableBrand<{}, M>} + */ + const prepareSingleton = (baggage, kindName, methods, options = undefined) => + prepareExo(baggage, kindName, undefined, methods, options); + harden(prepareSingleton); + + return harden({ + defineVirtualExoClass, + defineVirtualExoClassKit, + defineDurableExoClass, + defineDurableExoClassKit, + prepareExoClass, + prepareExoClassKit, + prepareExo, + prepareSingleton, - // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error -- https://github.com/Agoric/agoric-sdk/issues/4620 - // @ts-ignore could be instantiated with an arbitrary type - return provide(baggage, `the_${kindName}`, () => makeSingleton()); + provideKindHandle, + prepareKind, + prepareKindMulti, + }); }; -harden(prepareExo); + +const globalExoUtils = makeExoUtils(globalVatData); + +export const { + defineVirtualExoClass, + defineVirtualExoClassKit, + defineDurableExoClass, + defineDurableExoClassKit, + prepareExoClass, + prepareExoClassKit, + prepareExo, + prepareSingleton, +} = globalExoUtils; /** - * @template {Record} M methods - * @deprecated Use prepareExo instead. - * @param {Baggage} baggage - * @param {string} kindName - * @param {M} methods - * @param {DefineKindOptions<{ self: M }>} [options] - * @returns {M & RemotableBrand<{}, M>} + * @deprecated Use Exos/ExoClasses instead of Kinds */ -export const prepareSingleton = ( - baggage, - kindName, - methods, - options = undefined, -) => prepareExo(baggage, kindName, undefined, methods, options); -harden(prepareSingleton); +export const { provideKindHandle, prepareKind, prepareKindMulti } = + globalExoUtils; diff --git a/packages/vat-data/src/vat-data-bindings.js b/packages/vat-data/src/vat-data-bindings.js index 7cacca40144..8a7fd78bc41 100644 --- a/packages/vat-data/src/vat-data-bindings.js +++ b/packages/vat-data/src/vat-data-bindings.js @@ -29,6 +29,9 @@ if ('VatData' in globalThis) { }; } +const VatDataExport = VatDataGlobal; +export { VatDataExport as VatData }; + /** * @deprecated Use Exos/ExoClasses instead of Kinds */ @@ -140,35 +143,60 @@ harden(partialAssign); */ export const provide = provideLazy; -/** - * @param {import('./types').Baggage} baggage - * @param {string} name - * @param {Omit} options - */ -export const provideDurableMapStore = (baggage, name, options = {}) => - provide(baggage, name, () => - makeScalarBigMapStore(name, { durable: true, ...options }), - ); -harden(provideDurableMapStore); +// TODO: Find a good home for this function used by @agoric/vat-data and testing code +export const makeStoreUtils = VatData => { + const { + // eslint-disable-next-line no-shadow -- these literally do shadow the globals + makeScalarBigMapStore, + // eslint-disable-next-line no-shadow -- these literally do shadow the globals + makeScalarBigWeakMapStore, + // eslint-disable-next-line no-shadow -- these literally do shadow the globals + makeScalarBigSetStore, + } = VatData; -/** - * @param {import('./types').Baggage} baggage - * @param {string} name - * @param {Omit} options - */ -export const provideDurableWeakMapStore = (baggage, name, options = {}) => - provide(baggage, name, () => - makeScalarBigWeakMapStore(name, { durable: true, ...options }), - ); -harden(provideDurableWeakMapStore); + /** + * @param {import('./types').Baggage} baggage + * @param {string} name + * @param {Omit} options + */ + const provideDurableMapStore = (baggage, name, options = {}) => + provide(baggage, name, () => + makeScalarBigMapStore(name, { durable: true, ...options }), + ); + harden(provideDurableMapStore); -/** - * @param {import('./types').Baggage} baggage - * @param {string} name - * @param {Omit} options - */ -export const provideDurableSetStore = (baggage, name, options = {}) => - provide(baggage, name, () => - makeScalarBigSetStore(name, { durable: true, ...options }), - ); -harden(provideDurableSetStore); + /** + * @param {import('./types').Baggage} baggage + * @param {string} name + * @param {Omit} options + */ + const provideDurableWeakMapStore = (baggage, name, options = {}) => + provide(baggage, name, () => + makeScalarBigWeakMapStore(name, { durable: true, ...options }), + ); + harden(provideDurableWeakMapStore); + + /** + * @param {import('./types').Baggage} baggage + * @param {string} name + * @param {Omit} options + */ + const provideDurableSetStore = (baggage, name, options = {}) => + provide(baggage, name, () => + makeScalarBigSetStore(name, { durable: true, ...options }), + ); + harden(provideDurableSetStore); + + return harden({ + provideDurableMapStore, + provideDurableWeakMapStore, + provideDurableSetStore, + }); +}; + +const globalStoreUtils = makeStoreUtils(VatDataGlobal); +export const { + provideDurableMapStore, + provideDurableWeakMapStore, + provideDurableSetStore, +} = globalStoreUtils; From 261e0701f5e81fb96bb79aa4e65d7b9c86cc9c5b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 13 Mar 2023 21:21:56 -0400 Subject: [PATCH 19/25] test(swingset-liveslots): Expand the signature of setupTestLiveslots Ref #6523 --- .../test/liveslots-helpers.js | 38 +++++++++++---- .../test/storeGC/test-lifecycle.js | 16 +++---- .../test/storeGC/test-refcount-management.js | 14 +++--- .../test/storeGC/test-scalar-store-kind.js | 2 +- .../test/storeGC/test-weak-key.js | 6 +-- .../swingset-liveslots/test/test-baggage.js | 2 +- .../test/test-initial-vrefs.js | 6 ++- .../virtual-objects/test-virtualObjectGC.js | 48 +++++++++---------- .../tools/vo-test-harness.js | 3 +- 9 files changed, 77 insertions(+), 58 deletions(-) diff --git a/packages/swingset-liveslots/test/liveslots-helpers.js b/packages/swingset-liveslots/test/liveslots-helpers.js index 40904d9c83a..626f10d4200 100644 --- a/packages/swingset-liveslots/test/liveslots-helpers.js +++ b/packages/swingset-liveslots/test/liveslots-helpers.js @@ -15,11 +15,13 @@ import { import { kser } from './kmarshal.js'; /** - * @param {boolean} [skipLogging = false] + * @param {object} [options] + * @param {boolean} [options.skipLogging = false] + * @param {Map} [options.kvStore = new Map()] */ -export function buildSyscall(skipLogging) { +export function buildSyscall(options = {}) { + const { skipLogging = false, kvStore: fakestore = new Map() } = options; const log = []; - const fakestore = new Map(); let sortedKeys; let priorKeyReturned; let priorKeyIndex; @@ -152,23 +154,38 @@ export async function makeDispatch( return { dispatch, testHooks }; } -function makeRPMaker() { - let idx = 0; +function makeRPMaker(nextNumber = 1) { + let idx = nextNumber - 1; return () => { idx += 1; return `p-${idx}`; }; } +/** + * @param {import('ava').ExecutionContext} t + * @param {Function} buildRootObject + * @param {string} vatName + * @param {object} [options] + * @param {boolean} [options.forceGC] + * @param {Map} [options.kvStore = new Map()] + * @param {number} [options.nextPromiseImportNumber] + * @param {boolean} [options.skipLogging = false] + */ export async function setupTestLiveslots( t, buildRootObject, vatName, - forceGC, - skipLogging, + options = {}, ) { - const { log, syscall, fakestore } = buildSyscall(skipLogging); - const nextRP = makeRPMaker(); + const { + forceGC, + kvStore = new Map(), + nextPromiseImportNumber, + skipLogging = false, + } = options; + const { log, syscall, fakestore } = buildSyscall({ skipLogging, kvStore }); + const nextRP = makeRPMaker(nextPromiseImportNumber); const { dispatch, testHooks } = await makeDispatch( syscall, buildRootObject, @@ -215,7 +232,7 @@ export async function setupTestLiveslots( for (const [vpid, rejected, value] of l.resolutions) { if (vpid === rp) { if (rejected) { - throw Error(`vpid ${vpid} rejected with ${value}`); + throw Error(`vpid ${rp} rejected with ${value}`); } else { return value; // resolved successfully } @@ -249,6 +266,7 @@ export async function setupTestLiveslots( return { v, + dispatch, dispatchMessage, dispatchMessageSuccessfully, dispatchDropExports, diff --git a/packages/swingset-liveslots/test/storeGC/test-lifecycle.js b/packages/swingset-liveslots/test/storeGC/test-lifecycle.js index e7de7cef5da..72691cbf75d 100644 --- a/packages/swingset-liveslots/test/storeGC/test-lifecycle.js +++ b/packages/swingset-liveslots/test/storeGC/test-lifecycle.js @@ -71,7 +71,7 @@ test.serial('store lifecycle 1', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); // lerv -> Lerv Create store @@ -100,7 +100,7 @@ test.serial('store lifecycle 2', async t => { dispatchMessageSuccessfully, dispatchDropExports, dispatchRetireExports, - } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); // lerv -> Lerv Create store await dispatchMessageSuccessfully('makeAndHold'); @@ -176,7 +176,7 @@ test.serial('store lifecycle 3', async t => { dispatchMessageSuccessfully, dispatchDropExports, dispatchRetireExports, - } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); // lerv -> Lerv Create store await dispatchMessageSuccessfully('makeAndHold'); @@ -213,7 +213,7 @@ test.serial('store lifecycle 3', async t => { // test 4: lerv -> Lerv -> LERv -> LeRv -> lerv test.serial('store lifecycle 4', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); // lerv -> Lerv Create store await dispatchMessageSuccessfully('makeAndHold'); @@ -245,7 +245,7 @@ test.serial('store lifecycle 5', async t => { dispatchMessageSuccessfully, dispatchDropExports, dispatchRetireExports, - } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); // lerv -> Lerv Create store await dispatchMessageSuccessfully('makeAndHold'); @@ -279,7 +279,7 @@ test.serial('store lifecycle 5', async t => { // test 6: lerv -> Lerv -> LERv -> LeRv -> LeRV -> LeRv -> LeRV -> leRV -> lerv test.serial('store lifecycle 6', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); // lerv -> Lerv Create store await dispatchMessageSuccessfully('makeAndHold'); @@ -324,7 +324,7 @@ test.serial('store lifecycle 6', async t => { // test 7: lerv -> Lerv -> LERv -> lERv -> LERv -> lERv -> lerv test.serial('store lifecycle 7', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); // lerv -> Lerv Create store await dispatchMessageSuccessfully('makeAndHold'); @@ -363,7 +363,7 @@ test.serial('store lifecycle 7', async t => { // test 8: lerv -> Lerv -> LERv -> LERV -> LERv -> LERV -> lERV -> lERv -> lerv test.serial('store lifecycle 8', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); // lerv -> Lerv Create store await dispatchMessageSuccessfully('makeAndHold'); diff --git a/packages/swingset-liveslots/test/storeGC/test-refcount-management.js b/packages/swingset-liveslots/test/storeGC/test-refcount-management.js index 6bf4950ac8b..c9a3772aef6 100644 --- a/packages/swingset-liveslots/test/storeGC/test-refcount-management.js +++ b/packages/swingset-liveslots/test/storeGC/test-refcount-management.js @@ -28,7 +28,7 @@ test.serial('store refcount management 1', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -100,7 +100,7 @@ test.serial('store refcount management 2', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -136,7 +136,7 @@ test.serial('store refcount management 3', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -176,7 +176,7 @@ test.serial('presence refcount management 1', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -224,7 +224,7 @@ test.serial('presence refcount management 2', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -280,7 +280,7 @@ test.serial('remotable refcount management 1', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -316,7 +316,7 @@ test.serial('remotable refcount management 2', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; diff --git a/packages/swingset-liveslots/test/storeGC/test-scalar-store-kind.js b/packages/swingset-liveslots/test/storeGC/test-scalar-store-kind.js index a34025913e3..e6b427e15db 100644 --- a/packages/swingset-liveslots/test/storeGC/test-scalar-store-kind.js +++ b/packages/swingset-liveslots/test/storeGC/test-scalar-store-kind.js @@ -18,7 +18,7 @@ test.serial('assert known scalarMapStore ID', async t => { // registered. Check it explicity here. If this test fails, consider // updating `mapRef()` to use the new value. - const { testHooks } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { testHooks } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const id = testHooks.obtainStoreKindID('scalarMapStore'); t.is(id, 2); t.is(mapRef('INDEX'), 'o+v2/INDEX'); diff --git a/packages/swingset-liveslots/test/storeGC/test-weak-key.js b/packages/swingset-liveslots/test/storeGC/test-weak-key.js index a816a897954..c53dcd22785 100644 --- a/packages/swingset-liveslots/test/storeGC/test-weak-key.js +++ b/packages/swingset-liveslots/test/storeGC/test-weak-key.js @@ -23,7 +23,7 @@ test.serial('verify store weak key GC', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -108,7 +108,7 @@ test.serial('verify weakly held value GC', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; @@ -147,7 +147,7 @@ test.serial('verify weakly held value GC', async t => { // prettier-ignore test.serial('verify presence weak key GC', async t => { const { v, dispatchMessage, dispatchRetireImports, testHooks } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const { fakestore } = v; const presenceRef = 'o-5'; // Presence5 diff --git a/packages/swingset-liveslots/test/test-baggage.js b/packages/swingset-liveslots/test/test-baggage.js index 61f44028fc7..2f3647e0912 100644 --- a/packages/swingset-liveslots/test/test-baggage.js +++ b/packages/swingset-liveslots/test/test-baggage.js @@ -22,7 +22,7 @@ test.serial('exercise baggage', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const { fakestore } = v; diff --git a/packages/swingset-liveslots/test/test-initial-vrefs.js b/packages/swingset-liveslots/test/test-initial-vrefs.js index 2b5371f4c7a..04ce20281a6 100644 --- a/packages/swingset-liveslots/test/test-initial-vrefs.js +++ b/packages/swingset-liveslots/test/test-initial-vrefs.js @@ -51,7 +51,9 @@ function buildRootObject(vatPowers, vatParameters, baggage) { } test('initial vatstore contents', async t => { - const { v } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v } = await setupTestLiveslots(t, buildRootObject, 'bob', { + forceGC: true, + }); const { fakestore } = v; const get = key => fakestore.get(key); @@ -100,7 +102,7 @@ test('vrefs', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); // const { fakestore, dumpFakestore } = v; const { fakestore } = v; diff --git a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectGC.js b/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectGC.js index 49435cdbd6a..e07ef98bc8d 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectGC.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectGC.js @@ -417,7 +417,7 @@ async function voLifeCycleTest1(t, isf) { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const vref = facetRef(isf, thingVref(isf, 2), '1'); @@ -452,7 +452,7 @@ async function voLifeCycleTest2(t, isf) { dispatchMessageSuccessfully, dispatchDropExports, dispatchRetireExports, - } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -533,7 +533,7 @@ async function voLifeCycleTest3(t, isf) { dispatchMessageSuccessfully, dispatchDropExports, dispatchRetireExports, - } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -577,7 +577,7 @@ test.serial('VO lifecycle 3 faceted', async t => { // test 4: lerv -> Lerv -> LERv -> LeRv -> lerv async function voLifeCycleTest4(t, isf) { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -614,7 +614,7 @@ async function voLifeCycleTest5(t, isf) { dispatchMessageSuccessfully, dispatchDropExports, dispatchRetireExports, - } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -650,7 +650,7 @@ test.serial('VO lifecycle 5 faceted', async t => { // test 6: lerv -> Lerv -> LERv -> LeRv -> LeRV -> LeRv -> LeRV -> leRV -> lerv async function voLifeCycleTest6(t, isf) { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -699,7 +699,7 @@ test.serial('VO lifecycle 6 faceted', async t => { // test 7: lerv -> Lerv -> LERv -> lERv -> LERv -> lERv -> lerv async function voLifeCycleTest7(t, isf) { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -740,7 +740,7 @@ test.serial('VO lifecycle 7 faceted', async t => { // test 8: lerv -> Lerv -> LERv -> LERV -> LERv -> LERV -> lERV -> lERv -> lerv async function voLifeCycleTest8(t, isf) { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -792,7 +792,7 @@ test.serial('VO multifacet export 1', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const vref = facetRef(true, thingVref(true, 2), '1'); @@ -810,7 +810,7 @@ test.serial('VO multifacet export 1', async t => { // multifacet export test 2a: export A, drop A, retire A test.serial('VO multifacet export 2a', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(true, thingVref(true, 2), '0'); const thingA = kslot(vref, 'thing facetA'); @@ -838,7 +838,7 @@ test.serial('VO multifacet export 2a', async t => { // multifacet export test 2b: export B, drop B, retire B test.serial('VO multifacet export 2b', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(true, thingVref(true, 2), '1'); const thingB = kslot(vref, 'thing facetB'); @@ -865,7 +865,7 @@ test.serial('VO multifacet export 2b', async t => { // multifacet export test 3abba: export A, export B, drop B, drop A, retire test.serial('VO multifacet export 3abba', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vrefA = facetRef(true, thingVref(true, 2), '0'); const thingA = kslot(vrefA, 'thing facetA'); const vrefB = facetRef(true, thingVref(true, 2), '1'); @@ -903,7 +903,7 @@ test.serial('VO multifacet export 3abba', async t => { // multifacet export test 3abab: export A, export B, drop A, drop B, retire test.serial('VO multifacet export 3abab', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vrefA = facetRef(true, thingVref(true, 2), '0'); const thingA = kslot(vrefA, 'thing facetA'); const vrefB = facetRef(true, thingVref(true, 2), '1'); @@ -943,7 +943,7 @@ test.serial('VO multifacet markers only', async t => { t, buildRootObject, 'bob', - true, + { forceGC: true }, ); const vrefA = facetRef(true, `${markerBaseRef}/1`, '0'); const { baseRef } = parseVatSlot(vrefA); @@ -961,7 +961,7 @@ test.serial('VO multifacet markers only', async t => { // prettier-ignore async function voRefcountManagementTest1(t, isf) { - const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const { baseRef } = parseVatSlot(vref); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -1002,7 +1002,7 @@ test.serial('VO refcount management 1 faceted', async t => { // prettier-ignore async function voRefcountManagementTest2(t, isf) { - const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const { baseRef } = parseVatSlot(vref); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -1043,7 +1043,7 @@ test.serial('VO refcount management 2 faceted', async t => { // prettier-ignore async function voRefcountManagementTest3(t, isf) { - const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const { baseRef } = parseVatSlot(vref); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); @@ -1093,7 +1093,7 @@ test.serial('VO refcount management 3 faceted', async t => { // prettier-ignore test.serial('presence refcount management 1', async t => { - const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const { fakestore } = v; const vref = 'o-5'; @@ -1132,7 +1132,7 @@ test.serial('presence refcount management 1', async t => { // prettier-ignore test.serial('presence refcount management 2', async t => { - const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const { fakestore } = v; const vref = 'o-5'; @@ -1170,7 +1170,7 @@ test.serial('presence refcount management 2', async t => { // prettier-ignore test.serial('remotable refcount management 1', async t => { - const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const { fakestore } = v; // holder Kind is the next-to-last created kind, which gets idCounters.exportID-2 @@ -1213,7 +1213,7 @@ test.serial('remotable refcount management 1', async t => { // prettier-ignore test.serial('remotable refcount management 2', async t => { - const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const { fakestore } = v; const holderKindID = JSON.parse(fakestore.get(`idCounters`)).exportID - 2; @@ -1234,7 +1234,7 @@ test.serial('remotable refcount management 2', async t => { // prettier-ignore async function voWeakKeyGCTest(t, isf) { - const { v, dispatchMessageSuccessfully, testHooks } = await setupTestLiveslots(t, buildRootObject, 'bob', true); + const { v, dispatchMessageSuccessfully, testHooks } = await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = facetRef(isf, thingVref(isf, 2), '1'); const thing = kslot(vref, isf ? 'thing facetB' : 'thing'); const { baseRef } = parseVatSlot(vref); @@ -1265,7 +1265,7 @@ test.serial('verify VO weak key GC faceted', async t => { // prettier-ignore test.serial('verify presence weak key GC', async t => { const { v, dispatchMessageSuccessfully, dispatchRetireImports, testHooks } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const vref = 'o-5'; const presence = kslot(vref, 'thing'); // hold a Presence weakly by a VOAwareWeak(Map/Set), also by RAM @@ -1316,7 +1316,7 @@ test.serial('verify presence weak key GC', async t => { // prettier-ignore test.serial('VO holding non-VO', async t => { const { v, dispatchMessageSuccessfully, dispatchDropExports, dispatchRetireExports } = - await setupTestLiveslots(t, buildRootObject, 'bob', true); + await setupTestLiveslots(t, buildRootObject, 'bob', { forceGC: true }); const { fakestore } = v; // lerv -> Lerv Create non-VO diff --git a/packages/swingset-liveslots/tools/vo-test-harness.js b/packages/swingset-liveslots/tools/vo-test-harness.js index cb13aee62e4..4885246d7d9 100644 --- a/packages/swingset-liveslots/tools/vo-test-harness.js +++ b/packages/swingset-liveslots/tools/vo-test-harness.js @@ -131,8 +131,7 @@ export async function runVOTest(t, prepare, makeTestObject, testTestObject) { t, buildRootObject, 'bob', - true, - true, + { forceGC: true, skipLogging: true }, ); await dispatchMessage('makeAndHold'); From 60a8375c7b6f69cb019adf08c6c51be14a9d2da9 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 13 Mar 2023 21:22:40 -0400 Subject: [PATCH 20/25] test(swingset-liveslots): Add a default cache size to makeFakeVirtualStuff --- .../tools/fakeVirtualSupport.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/swingset-liveslots/tools/fakeVirtualSupport.js b/packages/swingset-liveslots/tools/fakeVirtualSupport.js index 89c91d0b541..6db39bd5cfd 100644 --- a/packages/swingset-liveslots/tools/fakeVirtualSupport.js +++ b/packages/swingset-liveslots/tools/fakeVirtualSupport.js @@ -291,17 +291,22 @@ export function makeFakeWatchedPromiseManager( * Configure virtual stuff with relaxed durability rules and fake liveslots * * @param {object} [options] + * @param {number} [options.cacheSize=3] * @param {boolean} [options.relaxDurabilityRules=true] - * @param {number} [options.cacheSize] */ export function makeFakeVirtualStuff(options = {}) { - const fakeStuff = makeFakeLiveSlotsStuff(options); - const { relaxDurabilityRules = true } = options; + const actualOptions = { + cacheSize: 3, + relaxDurabilityRules: true, + ...options, + }; + const { relaxDurabilityRules } = actualOptions; + const fakeStuff = makeFakeLiveSlotsStuff(actualOptions); const vrm = makeFakeVirtualReferenceManager(fakeStuff, relaxDurabilityRules); - const vom = makeFakeVirtualObjectManager(vrm, fakeStuff, options); + const vom = makeFakeVirtualObjectManager(vrm, fakeStuff, actualOptions); vom.initializeKindHandleKind(); fakeStuff.setVrm(vrm); - const cm = makeFakeCollectionManager(vrm, fakeStuff, options); + const cm = makeFakeCollectionManager(vrm, fakeStuff, actualOptions); const wpm = makeFakeWatchedPromiseManager(vrm, vom, cm, fakeStuff); return { fakeStuff, vrm, vom, cm, wpm }; } From 9207f4495f7f9c4457b4f1fa45a87087b5d680e2 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Wed, 15 Mar 2023 22:31:30 -0400 Subject: [PATCH 21/25] test(swingset-liveslots): Add liveslots-level handled promises tests --- .../test/test-handled-promises.js | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 packages/swingset-liveslots/test/test-handled-promises.js diff --git a/packages/swingset-liveslots/test/test-handled-promises.js b/packages/swingset-liveslots/test/test-handled-promises.js new file mode 100644 index 00000000000..fefb3b17e12 --- /dev/null +++ b/packages/swingset-liveslots/test/test-handled-promises.js @@ -0,0 +1,294 @@ +/* eslint-disable no-await-in-loop, @jessie.js/no-nested-await */ +import test from 'ava'; +import '@endo/init/debug.js'; + +import { Far } from '@endo/marshal'; +import { Fail } from '@agoric/assert'; +import { M } from '@agoric/store'; +import { makePromiseKit } from '@endo/promise-kit'; +// import { makeStoreUtils } from '../../vat-data/src/vat-data-bindings.js'; +import { makeExoUtils } from '../../vat-data/src/exo-utils.js'; +import { kslot, kser } from './kmarshal.js'; +import { setupTestLiveslots } from './liveslots-helpers.js'; +import { makeResolve, makeReject } from './util.js'; + +// eslint-disable-next-line no-underscore-dangle, no-nested-ternary +const _compareByKey = (a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0); + +// cf. packages/SwingSet/test/vat-durable-promise-watcher.js +const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => { + const { VatData } = vatPowers; + const { watchPromise } = VatData; + const { prepareExo } = makeExoUtils(VatData); + // const { makeScalarBigMapStore } = makeStoreUtils(VatData); + const PromiseWatcherI = M.interface('ExtraArgPromiseWatcher', { + onFulfilled: M.call(M.any(), M.string()).returns(), + onRejected: M.call(M.any(), M.string()).returns(), + }); + const watcher = prepareExo( + baggage, + 'DurablePromiseIgnorer', + PromiseWatcherI, + { + onFulfilled(_value, _name) {}, + onRejected(_reason, _name) {}, + }, + ); + + const localPromises = new Map(); + + return Far('root', { + exportPromise: () => [Promise.resolve()], + createLocalPromise: (name, fulfillment, rejection) => { + !localPromises.has(name) || Fail`local promise already exists: ${name}`; + const { promise, resolve, reject } = makePromiseKit(); + if (fulfillment !== undefined) { + resolve(fulfillment); + } else if (rejection !== undefined) { + reject(rejection); + } + localPromises.set(name, promise); + return `created local promise: ${name}`; + }, + watchLocalPromise: name => { + localPromises.has(name) || Fail`local promise not found: ${name}`; + watchPromise(localPromises.get(name), watcher, name); + return `watched local promise: ${name}`; + }, + }); +}; +const kvStoreDataV1 = Object.entries({ + baggageID: 'o+d6/1', + idCounters: '{"exportID":11,"collectionID":5,"promiseID":9}', + kindIDID: '1', + storeKindIDTable: + '{"scalarMapStore":2,"scalarWeakMapStore":3,"scalarSetStore":4,"scalarWeakSetStore":5,"scalarDurableMapStore":6,"scalarDurableWeakMapStore":7,"scalarDurableSetStore":8,"scalarDurableWeakSetStore":9}', + 'vc.1.sDurablePromiseIgnorer_kindHandle': + '{"body":"#\\"$0.Alleged: kind\\"","slots":["o+d1/10"]}', + 'vc.1.sthe_DurablePromiseIgnorer': + '{"body":"#\\"$0.Alleged: DurablePromiseIgnorer\\"","slots":["o+d10/1"]}', + 'vc.1.|entryCount': '2', + 'vc.1.|label': 'baggage', + 'vc.1.|nextOrdinal': '1', + 'vc.1.|schemata': + '{"body":"#[{\\"#tag\\":\\"match:string\\",\\"payload\\":[]}]","slots":[]}', + // non-durable + // 'vc.2.sp+6': '{"body":"#\\"&0\\"","slots":["p+6"]}', + // 'vc.2.|entryCount': '1', + // 'vc.2.|label': 'promiseRegistrations', + // 'vc.2.|nextOrdinal': '1', + // 'vc.2.|schemata': '{"body":"#[{\\"#tag\\":\\"match:scalar\\",\\"payload\\":\\"#undefined\\"}]","slots":[]}', + 'vc.3.|entryCount': '0', + 'vc.3.|label': 'promiseWatcherByKind', + 'vc.3.|nextOrdinal': '1', + 'vc.3.|schemata': + '{"body":"#[{\\"#tag\\":\\"match:scalar\\",\\"payload\\":\\"#undefined\\"}]","slots":[]}', + 'vc.4.sp+6': + '{"body":"#[[\\"$0.Alleged: DurablePromiseIgnorer\\",\\"orphaned\\"]]","slots":["o+d10/1"]}', + 'vc.4.sp-8': + '{"body":"#[[\\"$0.Alleged: DurablePromiseIgnorer\\",\\"unresolved\\"]]","slots":["o+d10/1"]}', + 'vc.4.sp-9': + '{"body":"#[[\\"$0.Alleged: DurablePromiseIgnorer\\",\\"late-rejected\\"]]","slots":["o+d10/1"]}', + 'vc.4.|entryCount': '3', + 'vc.4.|label': 'watchedPromises', + 'vc.4.|nextOrdinal': '1', + 'vc.4.|schemata': + '{"body":"#[{\\"#tag\\":\\"match:and\\",\\"payload\\":[{\\"#tag\\":\\"match:scalar\\",\\"payload\\":\\"#undefined\\"},{\\"#tag\\":\\"match:string\\",\\"payload\\":[]}]}]","slots":[]}', + 'vom.dkind.10': + '{"kindID":"10","tag":"DurablePromiseIgnorer","nextInstanceID":2,"unfaceted":true}', + 'vom.o+d10/1': '{}', + 'vom.rc.o+d1/10': '1', + 'vom.rc.o+d10/1': '3', + 'vom.rc.o+d6/1': '1', + 'vom.rc.o+d6/3': '1', + 'vom.rc.o+d6/4': '1', + watchedPromiseTableID: 'o+d6/4', + watcherTableID: 'o+d6/3', +}); +const kvStoreDataV1VpidsToReject = ['p+6', 'p-9']; +const kvStoreDataV1KeysToDelete = ['vc.4.sp+6', 'vc.4.sp-9']; +const kvStoreDataV1VpidsToKeep = ['p-8']; +const kvStoreDataV1KeysToKeep = ['vc.4.sp-8']; + +test('past-incarnation watched promises', async t => { + const kvStore = new Map(); + let { v, dispatch, dispatchMessage } = await setupTestLiveslots( + t, + buildPromiseWatcherRootObject, + 'durable-promise-watcher', + { kvStore }, + ); + let vatLogs = v.log; + + // Anchor promise counters upon which the other assertions depend. + const firstPImport = 1; + // cf. src/liveslots.js:initialIDCounters + const firstPExport = 5; + let lastPImport = firstPImport - 1; + let lastPExport = firstPExport - 1; + const nextPImport = () => (lastPImport += 1); + const nextPExport = () => (lastPExport += 1); + // Ignore vatstore syscalls. + const getDispatchLogs = () => + vatLogs.splice(0).filter(m => !m.type.startsWith('vatstore')); + const settlementMessage = (vpid, rejected, value) => ({ + type: 'resolve', + resolutions: [[vpid, rejected, kser(value)]], + }); + const fulfillmentMessage = (vpid, value) => + settlementMessage(vpid, false, value); + const rejectionMessage = (vpid, value) => + settlementMessage(vpid, true, value); + const subscribeMessage = vpid => ({ + type: 'subscribe', + target: vpid, + }); + vatLogs.length = 0; + await dispatchMessage('exportPromise'); + t.deepEqual(getDispatchLogs(), [ + fulfillmentMessage(`p-${nextPImport()}`, [kslot(`p+${nextPExport()}`)]), + fulfillmentMessage(`p+${lastPExport}`, undefined), + ]); + + const S = 'settlement'; + await dispatchMessage('createLocalPromise', 'orphaned'); + t.deepEqual(getDispatchLogs(), [ + fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: orphaned'), + ]); + await dispatchMessage('createLocalPromise', 'fulfilled', S); + t.deepEqual(getDispatchLogs(), [ + fulfillmentMessage( + `p-${nextPImport()}`, + 'created local promise: fulfilled', + ), + ]); + await dispatchMessage('createLocalPromise', 'rejected', undefined, S); + t.deepEqual(getDispatchLogs(), [ + fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: rejected'), + ]); + t.deepEqual( + lastPImport - firstPImport + 1, + 4, + 'imported 4 promises (1 per dispatch)', + ); + t.deepEqual(lastPExport - firstPExport + 1, 1, 'exported 1 promise: first'); + + await dispatchMessage('watchLocalPromise', 'orphaned'); + t.deepEqual(getDispatchLogs(), [ + subscribeMessage(`p+${nextPExport()}`), + fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: orphaned'), + ]); + await dispatchMessage('watchLocalPromise', 'fulfilled'); + t.deepEqual(getDispatchLogs(), [ + subscribeMessage(`p+${nextPExport()}`), + fulfillmentMessage( + `p-${nextPImport()}`, + 'watched local promise: fulfilled', + ), + fulfillmentMessage(`p+${lastPExport}`, S), + ]); + await dispatchMessage('watchLocalPromise', 'rejected'); + t.deepEqual(getDispatchLogs(), [ + subscribeMessage(`p+${nextPExport()}`), + fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: rejected'), + rejectionMessage(`p+${lastPExport}`, S), + ]); + t.deepEqual( + lastPImport - firstPImport + 1, + 7, + 'imported 7 promises (1 per dispatch)', + ); + t.deepEqual( + lastPExport - firstPExport + 1, + 4, + 'exported 4 promises: first, orphaned, fulfilled, rejected', + ); + + // Simulate upgrade by starting from the non-empty kvStore. + // t.log(Object.fromEntries([...kvStore.entries()].sort(_compareByKey))); + const clonedStore = new Map(kvStore); + ({ v, dispatch, dispatchMessage } = await setupTestLiveslots( + t, + buildPromiseWatcherRootObject, + 'durable-promise-watcher-v2', + { kvStore: clonedStore, nextPromiseImportNumber: lastPImport + 1 }, + )); + vatLogs = v.log; + + // Simulate kernel rejection of promises orphaned by termination/upgrade of their decider vat. + const expectedDeletions = [...clonedStore.entries()].filter(entry => + entry[1].includes('orphaned'), + ); + t.true(expectedDeletions.length >= 1); + await dispatch( + makeReject(`p+${firstPExport + 1}`, kser('tomorrow never came')), + ); + for (const [key, value] of expectedDeletions) { + t.false(clonedStore.has(key), `entry should be removed: ${key}: ${value}`); + } + + // Verify that the data is still in loadable condition. + const finalClonedStore = new Map(clonedStore); + ({ v, dispatch, dispatchMessage } = await setupTestLiveslots( + t, + buildPromiseWatcherRootObject, + 'durable-promise-watcher-final', + { kvStore: finalClonedStore, nextPromiseImportNumber: lastPImport + 1 }, + )); + vatLogs = v.log; + vatLogs.length = 0; + await dispatchMessage('createLocalPromise', 'final', S); + await dispatchMessage('watchLocalPromise', 'final'); + t.deepEqual(getDispatchLogs(), [ + fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: final'), + subscribeMessage(`p+${nextPExport()}`), + fulfillmentMessage(`p-${nextPImport()}`, 'watched local promise: final'), + fulfillmentMessage(`p+${lastPExport}`, S), + ]); +}); + +test('past-incarnation watched promises from original-format kvStore', async t => { + const kvStore = new Map(kvStoreDataV1); + for (const key of [ + ...kvStoreDataV1KeysToDelete, + ...kvStoreDataV1KeysToKeep, + ]) { + t.true(kvStore.has(key), `key must be initially present: ${key}`); + } + + let { v, dispatch, dispatchMessage } = await setupTestLiveslots( + t, + buildPromiseWatcherRootObject, + 'durable-promise-watcher', + { kvStore, nextPromiseImportNumber: 100 }, + ); + let vatLogs = v.log; + for (const vpid of kvStoreDataV1VpidsToReject) { + await dispatch(makeReject(vpid, kser('tomorrow never came'))); + } + for (const key of kvStoreDataV1KeysToDelete) { + t.false(kvStore.has(key), `key should be removed: ${key}`); + } + for (const key of kvStoreDataV1KeysToKeep) { + t.true(kvStore.has(key), `key should remain: ${key}`); + } + + // Verify that the data is still in loadable condition. + const finalClonedStore = new Map(kvStore); + // eslint-disable-next-line no-unused-vars + ({ v, dispatch, dispatchMessage } = await setupTestLiveslots( + t, + buildPromiseWatcherRootObject, + 'durable-promise-watcher-final', + { kvStore: finalClonedStore, nextPromiseImportNumber: 200 }, + )); + vatLogs = v.log; + console.log(...vatLogs); + vatLogs.length = 0; + for (const vpid of kvStoreDataV1VpidsToKeep) { + await dispatch(makeResolve(vpid, kser('finally'))); + } + for (const key of kvStoreDataV1KeysToKeep) { + t.false(finalClonedStore.has(key), `key should be removed: ${key}`); + } +}); From 8327856c524a75076ac300b0a63ec7cde0f642f6 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 16 Mar 2023 00:51:44 -0400 Subject: [PATCH 22/25] chore: Minor jsdoc/eslint improvements --- packages/SwingSet/test/test-promises.js | 4 ++-- packages/swingset-liveslots/src/liveslots.js | 2 +- packages/swingset-liveslots/test/test-handled-promises.js | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/SwingSet/test/test-promises.js b/packages/SwingSet/test/test-promises.js index 8ed2724f390..89cc8e12c23 100644 --- a/packages/SwingSet/test/test-promises.js +++ b/packages/SwingSet/test/test-promises.js @@ -252,8 +252,8 @@ test('local promises are rejected by vat upgrade', async t => { }; const messageVat = (name, methodName, args) => run('messageVat', [{ name, methodName, args }]); - // eslint-disable-next-line no-underscore-dangle - const _messageObject = (presence, methodName, args) => + // eslint-disable-next-line no-unused-vars + const messageObject = (presence, methodName, args) => run('messageVatObject', [{ presence, methodName, args }]); const S = Symbol.for('passable'); diff --git a/packages/swingset-liveslots/src/liveslots.js b/packages/swingset-liveslots/src/liveslots.js index 82cf235a2dd..2699c8dee1e 100644 --- a/packages/swingset-liveslots/src/liveslots.js +++ b/packages/swingset-liveslots/src/liveslots.js @@ -557,7 +557,7 @@ function build( * adds it to exportedVPIDs and sets up handlers. * * @param {any} vref - * @returns {boolean} + * @returns {boolean} whether the vref was added to exportedVPIDs */ function maybeExportPromise(vref) { // we only care about new vpids diff --git a/packages/swingset-liveslots/test/test-handled-promises.js b/packages/swingset-liveslots/test/test-handled-promises.js index fefb3b17e12..07c101d23dd 100644 --- a/packages/swingset-liveslots/test/test-handled-promises.js +++ b/packages/swingset-liveslots/test/test-handled-promises.js @@ -12,8 +12,8 @@ import { kslot, kser } from './kmarshal.js'; import { setupTestLiveslots } from './liveslots-helpers.js'; import { makeResolve, makeReject } from './util.js'; -// eslint-disable-next-line no-underscore-dangle, no-nested-ternary -const _compareByKey = (a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0); +// eslint-disable-next-line no-unused-vars +const compareEntriesByKey = ([ka], [kb]) => (ka < kb ? -1 : 1); // cf. packages/SwingSet/test/vat-durable-promise-watcher.js const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => { @@ -205,7 +205,7 @@ test('past-incarnation watched promises', async t => { ); // Simulate upgrade by starting from the non-empty kvStore. - // t.log(Object.fromEntries([...kvStore.entries()].sort(_compareByKey))); + // t.log(Object.fromEntries([...kvStore.entries()].sort(compareEntriesByKey))); const clonedStore = new Map(kvStore); ({ v, dispatch, dispatchMessage } = await setupTestLiveslots( t, From 5a723d4761af8e034724d410a057814f352168fb Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 16 Mar 2023 16:06:12 -0400 Subject: [PATCH 23/25] fixup! docs(swingset-liveslots): Clarify VPID managment documentation --- packages/swingset-liveslots/src/vpid-tracking.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swingset-liveslots/src/vpid-tracking.md b/packages/swingset-liveslots/src/vpid-tracking.md index 80ada85849f..05a2714a0e0 100644 --- a/packages/swingset-liveslots/src/vpid-tracking.md +++ b/packages/swingset-liveslots/src/vpid-tracking.md @@ -82,7 +82,7 @@ Remember that the `slotToVal` registration uses a WeakRef, so being registered t * construct a promise `res` to capture the userspace-provided result * if the VPID is present in `importedVPIDs`: retrieve the `[resolve, reject]` pRec and use `resolve(res)` to forward eventual settlement of `res` to settlement of the previously-imported promise, then remove the VPID from `importedVPIDs` * else: register marshaller association between the VPID and `res` - * in either case, use `followForKernel` to add the VPID to `exportedVPIDs` and attach `.then(onFulfill, onReject)` callbacks that will map fulfillment/rejection to `syscall.resolve() + * in either case, use `followForKernel` to add the VPID to `exportedVPIDs` and attach `.then(onFulfill, onReject)` callbacks that will map fulfillment/rejection to `syscall.resolve()` If the serialization is for storage in virtual data, the act of storing the VPID will add the Promise to `remotableRefCounts`, which maintains a strong reference for as long as the VPID is held. When it is removed from virtual data (or the object/collection is deleted), the refcount will be decremented. When the refcount drops to zero, we perform the `exportedVPIDs`/`importedVPIDs` check and then maybe unregister the promise. From 3777b89832bfdd68dff5fa350e51e31a97411b9a Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 16 Mar 2023 16:16:26 -0400 Subject: [PATCH 24/25] test(swingset-liveslots): Remove the dependency on vat-data --- .../test/test-handled-promises.js | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/packages/swingset-liveslots/test/test-handled-promises.js b/packages/swingset-liveslots/test/test-handled-promises.js index 07c101d23dd..db1aafbaed8 100644 --- a/packages/swingset-liveslots/test/test-handled-promises.js +++ b/packages/swingset-liveslots/test/test-handled-promises.js @@ -1,13 +1,14 @@ -/* eslint-disable no-await-in-loop, @jessie.js/no-nested-await */ +/* eslint-disable no-await-in-loop, @jessie.js/no-nested-await, no-shadow */ import test from 'ava'; import '@endo/init/debug.js'; import { Far } from '@endo/marshal'; import { Fail } from '@agoric/assert'; -import { M } from '@agoric/store'; +import { M, provideLazy as provide } from '@agoric/store'; import { makePromiseKit } from '@endo/promise-kit'; -// import { makeStoreUtils } from '../../vat-data/src/vat-data-bindings.js'; -import { makeExoUtils } from '../../vat-data/src/exo-utils.js'; +// Disabled to avoid circular dependencies. +// import { makeStoreUtils } from '@agoric/vat-data/src/vat-data-bindings.js'; +// import { makeExoUtils } from '@agoric/vat-data/src/exo-utils.js'; import { kslot, kser } from './kmarshal.js'; import { setupTestLiveslots } from './liveslots-helpers.js'; import { makeResolve, makeReject } from './util.js'; @@ -15,6 +16,75 @@ import { makeResolve, makeReject } from './util.js'; // eslint-disable-next-line no-unused-vars const compareEntriesByKey = ([ka], [kb]) => (ka < kb ? -1 : 1); +// Paritally duplicates @agoric/vat-data to avoid circular dependencies. +const makeExoUtils = VatData => { + const { defineDurableKind, makeKindHandle, watchPromise } = VatData; + + const provideKindHandle = (baggage, kindName) => + provide(baggage, `${kindName}_kindHandle`, () => makeKindHandle(kindName)); + + const emptyRecord = harden({}); + const initEmpty = () => emptyRecord; + + const defineDurableExoClass = ( + kindHandle, + interfaceGuard, + init, + methods, + options, + ) => + defineDurableKind(kindHandle, init, methods, { + ...options, + thisfulMethods: true, + interfaceGuard, + }); + + const prepareExoClass = ( + baggage, + kindName, + interfaceGuard, + init, + methods, + options = undefined, + ) => + defineDurableExoClass( + provideKindHandle(baggage, kindName), + interfaceGuard, + init, + methods, + options, + ); + + const prepareExo = ( + baggage, + kindName, + interfaceGuard, + methods, + options = undefined, + ) => { + const makeSingleton = prepareExoClass( + baggage, + kindName, + interfaceGuard, + initEmpty, + methods, + options, + ); + return provide(baggage, `the_${kindName}`, () => makeSingleton()); + }; + + return { + defineDurableKind, + makeKindHandle, + watchPromise, + + provideKindHandle, + defineDurableExoClass, + prepareExoClass, + prepareExo, + }; +}; + // cf. packages/SwingSet/test/vat-durable-promise-watcher.js const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => { const { VatData } = vatPowers; @@ -283,7 +353,6 @@ test('past-incarnation watched promises from original-format kvStore', async t = { kvStore: finalClonedStore, nextPromiseImportNumber: 200 }, )); vatLogs = v.log; - console.log(...vatLogs); vatLogs.length = 0; for (const vpid of kvStoreDataV1VpidsToKeep) { await dispatch(makeResolve(vpid, kser('finally'))); From 8dab80ff5ec39902791897d22d2aa0d42596db6d Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Thu, 16 Mar 2023 18:58:46 -0400 Subject: [PATCH 25/25] docs(swingset-liveslots): Restore tildot example that might be important --- packages/swingset-liveslots/src/vpid-tracking.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swingset-liveslots/src/vpid-tracking.md b/packages/swingset-liveslots/src/vpid-tracking.md index 05a2714a0e0..208acc7a6cf 100644 --- a/packages/swingset-liveslots/src/vpid-tracking.md +++ b/packages/swingset-liveslots/src/vpid-tracking.md @@ -2,7 +2,7 @@ Kernels and vats communicate about promises by referring to their VPIDs: vat(-centric) promise IDs. These are strings like `p+12` and `p-23`. Like VOIDs (object IDs), the plus/minus sign indicates which side of the boundary allocated the number (`p+12` and `o+12` are allocated by the vat, `p-13` and `o-13` are allocated by the kernel). But where the object ID sign also indicates which side "owns" the object (i.e. where the behavior lives), the promise ID sign is generally irrelevant. -Instead, we care about which side holds the resolution authority for a promise (referred to as being its **decider**). This is not indicated by the VPID sign, and in fact is not necessarily static. Liveslots does not currently have any mechanism to allow one promise to be forwarded to another, but if it acquires this some day, then the decider of a promise could shift from kernel to vat to kernel again before it finally gets resolved. And there *are* sequences that allow a vat to receive a reference to a promise in method arguments before receiving a message for which that same VPID identifies the result promise (e.g., `{ promise: p1, resolve: resolveP1 } = makePromiseKit(); p2 = E(p1).push('queued'); await E(observer).push(p2); resolveP1(observer);` with an observer whose `push` returns a prompt response). In such cases, the decider is initially the kernel but later becomes the receiving vat. +Instead, we care about which side holds the resolution authority for a promise (referred to as being its **decider**). This is not indicated by the VPID sign, and in fact is not necessarily static. Liveslots does not currently have any mechanism to allow one promise to be forwarded to another, but if it acquires this some day, then the decider of a promise could shift from kernel to vat to kernel again before it finally gets resolved. And there *are* sequences that allow a vat to receive a reference to a promise in method arguments before receiving a message whose result promise uses that same VPID (e.g., `p1 = p2~.foo(); x~.bar(p1)`, then someone resolves `p2` to `x`. Consider also tildot-free code like `{ promise: p1, resolve: resolveP1 } = makePromiseKit(); p2 = E(p1).push('queued'); await E(observer).push(p2); resolveP1(observer);` with an observer whose `push` returns a prompt response). In such cases, the decider is initially the kernel but that authority is transferred to a vat later. Each Promise starts out in the "unresolved" state, then later transitions irrevocably to the "resolved" state. Liveslots frequently (but not always) pays attention to this transition by calling `then` to attach fulfillment/rejection settlement callbacks. To handle resolution cycles, liveslots remembers the resolution of old promises in a `WeakMap` for as long as the Promise exists. Consequently, for liveslots' purposes, every Promise is either resolved (a callback has fired and liveslots remembers the settlement), or unresolved (liveslots has not yet seen a resolution that settles it).