diff --git a/packages/SwingSet/src/liveslots/collectionManager.js b/packages/SwingSet/src/liveslots/collectionManager.js index 7a5c2454c58..e41f6b02e63 100644 --- a/packages/SwingSet/src/liveslots/collectionManager.js +++ b/packages/SwingSet/src/liveslots/collectionManager.js @@ -31,6 +31,12 @@ export function makeCollectionManager( serialize, unserialize, ) { + // TODO(#5058): we hold a list of all collections (both virtual and + // durable) in RAM, so we can delete the virtual ones during + // stopVat(), and tolerate subsequent GC-triggered duplication + // deletion without crashing. This needs to move to the DB to avoid + // the RAM consumption of a large number of collections. + const allCollectionObjIDs = new Set(); const storeKindIDToName = new Map(); const storeKindInfo = { @@ -566,9 +572,13 @@ export function makeCollectionManager( } function deleteCollection(vobjID) { + if (!allCollectionObjIDs.has(vobjID)) { + return false; // already deleted + } const { id, subid } = parseVatSlot(vobjID); const kindName = storeKindIDToName.get(`${id}`); const collection = summonCollectionInternal(false, 'GC', subid, kindName); + allCollectionObjIDs.delete(vobjID); const doMoreGC = collection.clearInternal(true); let priorKey = ''; @@ -583,6 +593,18 @@ export function makeCollectionManager( return doMoreGC; } + function deleteAllVirtualCollections() { + const vobjIDs = Array.from(allCollectionObjIDs).sort(); + for (const vobjID of vobjIDs) { + const { id } = parseVatSlot(vobjID); + const kindName = storeKindIDToName.get(`${id}`); + const { durable } = storeKindInfo[kindName]; + if (!durable) { + deleteCollection(vobjID); + } + } + } + function makeCollection(label, kindName, keySchema, valueSchema) { assert.typeof(label, 'string'); assert(storeKindInfo[kindName]); @@ -606,6 +628,7 @@ export function makeCollectionManager( JSON.stringify(serialize(harden(schemata))), ); syscall.vatstoreSet(prefixc(collectionID, '|label'), label); + allCollectionObjIDs.add(vobjID); return [ vobjID, @@ -843,6 +866,7 @@ export function makeCollectionManager( return harden({ initializeStoreKindInfo, + deleteAllVirtualCollections, makeScalarBigMapStore, makeScalarBigWeakMapStore, makeScalarBigSetStore, diff --git a/packages/SwingSet/src/liveslots/liveslots.js b/packages/SwingSet/src/liveslots/liveslots.js index 87eb951e796..f074ce4a43d 100644 --- a/packages/SwingSet/src/liveslots/liveslots.js +++ b/packages/SwingSet/src/liveslots/liveslots.js @@ -16,6 +16,7 @@ import { insistMessage } from '../lib/message.js'; import { makeVirtualReferenceManager } from './virtualReferences.js'; import { makeVirtualObjectManager } from './virtualObjectManager.js'; import { makeCollectionManager } from './collectionManager.js'; +import { releaseOldState } from './stop-vat.js'; const DEFAULT_VIRTUAL_OBJECT_CACHE_SIZE = 3; // XXX ridiculously small value to force churn for testing @@ -1265,32 +1266,6 @@ function build( vom.insistAllDurableKindsReconnected(); } - /** - * @returns { Promise } - */ - async function stopVat() { - assert(didStartVat); - assert(!didStopVat); - didStopVat = true; - - // 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 rejectCapData = m.serialize('vat upgraded'); - const vpids = Array.from(deciderVPIDs.keys()).sort(); - const rejections = vpids.map(vpid => [vpid, true, rejectCapData]); - if (rejections.length) { - syscall.resolve(rejections); - } - // eslint-disable-next-line no-use-before-define - await bringOutYourDead(); - } - /** * @param { VatDeliveryObject } delivery * @returns { void | Promise } @@ -1330,10 +1305,6 @@ function build( result = startVat(vpCapData); break; } - case 'stopVat': { - result = stopVat(); - break; - } default: assert.fail(X`unknown delivery type ${type}`); } @@ -1354,6 +1325,37 @@ function build( return undefined; } + /** + * @returns { Promise } + */ + async function stopVat() { + assert(didStartVat); + assert(!didStopVat); + didStopVat = true; + + try { + await releaseOldState({ + m, + deciderVPIDs, + syscall, + exportedRemotables, + addToPossiblyDeadSet, + slotToVal, + valToSlot, + dropExports, + retireExports, + vrm, + collectionManager, + bringOutYourDead, + vreffedObjectRegistry, + }); + } catch (e) { + console.log(`-- error during stopVat()`); + console.log(e); + throw e; + } + } + /** * Do things that should be done (such as flushing caches to disk) after a * dispatch has completed and user code has relinquished agency. @@ -1402,9 +1404,11 @@ function build( */ async function dispatch(delivery) { // We must short-circuit dispatch to bringOutYourDead here because it has to - // be async + // be async, same for stopVat if (delivery[0] === 'bringOutYourDead') { return meterControl.runWithoutMeteringAsync(bringOutYourDead); + } else if (delivery[0] === 'stopVat') { + return meterControl.runWithoutMeteringAsync(stopVat); } else { // Start user code running, record any internal liveslots errors. We do // *not* directly wait for the userspace function to complete, nor for diff --git a/packages/SwingSet/src/liveslots/stop-vat.js b/packages/SwingSet/src/liveslots/stop-vat.js new file mode 100644 index 00000000000..74b080f892c --- /dev/null +++ b/packages/SwingSet/src/liveslots/stop-vat.js @@ -0,0 +1,393 @@ +import { makeVatSlot, parseVatSlot } from '../lib/parseVatSlots.js'; + +// This file has tools to run during the last delivery of the old vat +// version, `dispatch.stopVat()`, just before an upgrade. It is +// responsible for deleting as much of the non-retained data as +// possible. The primary function is `releaseOldState()`, at the end; +// everything else is a helper function. + +// The only data that should be retained are those durable objects and +// collections which are transitively reachable from two sets of +// roots: the durable objects exported to other vats, and the +// "baggage" collection. All other durable objects, and *all* +// merely-virtual objects (regardless of export status) should be +// deleted. All imports which were kept alive by dropped object should +// also be dropped. + +// However, the possibility of cycles within durable storage means +// that a full cleanup requires a mark+sweep pass through all durable +// objects, which I think is too expensive for right now. + +// So instead, I'm going to settle on a cheaper `stopVat` which +// correctly drops durable objects and imports that were only kept +// alive by 1: RAM, 2: non-durable exports, or 3: non-durable +// objects/collections. It will require a walk through all non-durable +// objects and collections, but not a mark+sweep through all durable +// objects. + +// This cheaper form may leak some imports and storage, but should not +// allow the new vat to access anything it shouldn't, nor allow other +// vats to cause confusion in the new version (by referencing exports +// which the vat no longer remembers). + +const rootSlot = makeVatSlot('object', true, 0n); + +function rejectAllPromises({ m, deciderVPIDs, syscall }) { + // 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 rejectCapData = m.serialize('vat upgraded'); + const vpids = Array.from(deciderVPIDs.keys()).sort(); + const rejections = vpids.map(vpid => [vpid, true, rejectCapData]); + if (rejections.length) { + syscall.resolve(rejections); + } +} + +function identifyExportedRemotables( + vrefSet, + { exportedRemotables, valToSlot }, +) { + // Find all exported "Remotables": precious in-RAM objects declared + // with Far and sent as message argument or promise resolution. + // These are all doomed, except for the root object (which the new + // version will rebind). We'll pretend the kernel drops these in a + // minute. + + for (const r of exportedRemotables) { + const vref = valToSlot.get(r); + if (vref === rootSlot) { + // We know the new version can/will reattach a new root + // object, so if the kernel is still watching it, don't + // abandon it. We don't simulate a dispatch.dropExports, to + // preserve any vdata that might be keyed by it. But we do + // drop it from RAM, so we can collect any Presences or + // Representatives the Remotable had captured. + exportedRemotables.delete(r); + } else { + vrefSet.add(vref); + } + } +} + +function identifyExportedFacets(vrefSet, { syscall, vrm }) { + // Find all exported (non-durable) virtual object facets, which are + // doomed because merely-virtual objects don't survive upgrade. We + // walk the "export status" (vom.es.) portion of the DB to find the + // ones that are reachable ('r') by the kernel, ignoring the ones + // that are merely recognizable ('s'). We'll pretend the kernel + // drops these in a minute. + + const prefix = 'vom.es.'; + let [key, value] = syscall.vatstoreGetAfter('', prefix); + while (key) { + const baseRef = key.slice(prefix.length); + const parsed = parseVatSlot(baseRef); + assert(parsed.virtual && parsed.baseRef === baseRef, baseRef); + if (!vrm.isDurableKind(parsed.id)) { + if (value.length === 1) { + // single-facet + if (value === 'r') { + const vref = baseRef; + vrefSet.add(vref); + } + } else { + // multi-facet + for (let i = 0; i < value.length; i += 1) { + if (value[i] === 'r') { + const vref = `${baseRef}:${i}`; + vrefSet.add(vref); + } + } + } + } + [key, value] = syscall.vatstoreGetAfter(key, prefix); + } +} + +function abandonExports(vrefSet, tools) { + // Pretend the kernel dropped everything in the set. The Remotables + // will be removed from exportedRemotables. If the export was the + // only pillar keeping them alive, the objects will be deleted, + // decrefs will happen, and a bunch of stuff will be pushed onto + // possiblyDeadSet. The virtual objects will lose their export + // pillar, which may do the same. + + const { dropExports, retireExports, syscall } = tools; + const abandonedVrefs = Array.from(vrefSet).sort(); + dropExports(abandonedVrefs); + // also pretend the kernel retired them, so we retire them ourselves + retireExports(abandonedVrefs); + + // Now that we think they're gone, abandon them so the kernel agrees + syscall.abandonExports(abandonedVrefs); +} + +// eslint-disable-next-line no-unused-vars +function finalizeEverything(tools) { + const { slotToVal, addToPossiblyDeadSet, vreffedObjectRegistry } = tools; + + // The liveslots tables which might keep userspace objects alive + // are: + // * exportedRemotables + // * importedDevices + // * importedPromisesByPromiseID + // * pendingPromises + // * vrm.remotableRefCounts + // * vrm.vrefRecognizers (which points to virtualObjectMap which + // is a strong Map whose values might be + // Presences or Representatives) + + // Use slotToVal to find all the Presences, Remotables, and + // Representatives, and simulate the finalizer calls. This doesn't + // remove those objects from RAM, but it makes liveslots + // decref/drop/retire things as if they were. + + for (const baseRef of slotToVal.keys()) { + const p = parseVatSlot(baseRef); + if (p.type === 'object' && baseRef !== rootSlot) { + const wr = slotToVal.get(baseRef); + const val = wr.deref(); + if (val) { + // the object is still around, so pretend it went away + addToPossiblyDeadSet(baseRef); + // and remove it, else scanForDeadObjects() will think it was + // reintroduced + slotToVal.delete(baseRef); + // stop the real finalizer from firing + vreffedObjectRegistry.unregister(val); + } + // if !wr.deref(), there should already be a finalizer call queued + } + } +} + +function deleteVirtualObjectsWithoutDecref({ vrm, syscall }) { + // delete the data of all non-durable virtual objects, without + // attempting to decrement the refcounts of the surviving + // imports/durable-objects they might point to + + const prefix = 'vom.o+'; + let [key, _value] = syscall.vatstoreGetAfter('', prefix); + while (key) { + const baseRef = key.slice('vom.'.length); + const p = parseVatSlot(baseRef); + if (!vrm.isDurableKind(p.id)) { + syscall.vatstoreDelete(key); + } + [key, _value] = syscall.vatstoreGetAfter(key, prefix); + } +} + +// BEGIN: the following functions aren't ready for use yet + +// eslint-disable-next-line no-unused-vars +function deleteVirtualObjectsWithDecref({ syscall, vrm }) { + // Delete the data of all non-durable objects, building up a list of + // decrefs to apply to possibly-surviving imports and durable + // objects that the late virtual objects pointed to. We don't need + // to tell the kernel that we're deleting these: we already + // abandoned any that were exported. + + const durableDecrefs = new Map(); // baseRef -> count + const importDecrefs = new Map(); // baseRef -> count + const prefix = 'vom.o+'; + + let [key, value] = syscall.vatstoreGetAfter('', prefix); + while (key) { + const baseRef = key.slice('vom.'.length); + const p = parseVatSlot(baseRef); + if (!vrm.isDurableKind(p.id)) { + const raw = JSON.parse(value); + for (const capdata of Object.values(raw)) { + for (const vref of capdata.slots) { + const p2 = parseVatSlot(vref); + if (p2.virtual && vrm.isDurableKind(p2.id)) { + const count = durableDecrefs.get(p2.baseRef) || 0; + durableDecrefs.set(p2.baseRef, count + 1); + } + if (!p2.allocatedByVat) { + const count = importDecrefs.get(p2.baseRef) || 0; + importDecrefs.set(p2.baseRef, count + 1); + } + } + } + syscall.vatstoreDelete(key); + } + [key, value] = syscall.vatstoreGetAfter(key, prefix); + } + + // now decrement the DOs and imports that were held by the VOs, + // applying the whole delta at once (instead of doing multiple + // single decrefs) + const durableBaserefs = Array.from(durableDecrefs.keys()).sort(); + for (const baseRef of durableBaserefs) { + vrm.decRefCount(baseRef, durableBaserefs.get(baseRef)); + } + + const importVrefs = Array.from(importDecrefs.keys()).sort(); + for (const baseRef of importVrefs) { + vrm.decRefCount(baseRef, importDecrefs.get(baseRef)); + } +} + +// eslint-disable-next-line no-unused-vars +function deleteCollectionsWithDecref({ syscall, vrm }) { + // TODO this is not ready yet + + // Delete all items of all non-durable collections, counting up how + // many references their values had to imports and durable objects, + // so we can decref them in a large chunk at the end. + + // Walk prefix='vc.', extract vc.NN., look up whether collectionID + // NN is durable or not, skip the durables, delete the vc.NN.| + // metadata keys, walk the remaining vc.NN. keys, JSON.parse each, + // extract slots, update decrefcounts, delete + + // TODO: vrefs used as keys may maintain a refcount, and need to be + // handled specially. This code will probably get confused by such + // entries. + + const durableDecrefs = new Map(); // baseRef -> count + const importDecrefs = new Map(); // baseRef -> count + const prefix = 'vom.vc.'; + + let [key, value] = syscall.vatstoreGetAfter('', prefix); + while (key) { + const subkey = key.slice(prefix.length); // '2.|meta' or '2.ENCKEY' + const collectionID = subkey.slice(0, subkey.index('.')); // string + const subsubkey = subkey.slice(collectionID.length); // '|meta' or 'ENCKEY' + const isMeta = subsubkey.slice(0, 1) === '|'; + const isDurable = 'TODO'; // ask collectionManager about collectionID + if (!isDurable && !isMeta) { + for (const vref of JSON.parse(value).slots) { + const p = parseVatSlot(vref); + if (p.virtual && vrm.isDurableKind(p.id)) { + const count = durableDecrefs.get(p.baseRef) || 0; + durableDecrefs.set(p.baseRef, count + 1); + } + if (!p.allocatedByVat) { + const count = importDecrefs.get(p.baseRef) || 0; + importDecrefs.set(p.baseRef, count + 1); + } + } + } + syscall.vatstoreDelete(key); + + [key, value] = syscall.vatstoreGetAfter(key, prefix); + } + const durableBaserefs = Array.from(durableDecrefs.keys()).sort(); + for (const baseRef of durableBaserefs) { + vrm.decRefCount(baseRef, durableBaserefs.get(baseRef)); + } + + const importVrefs = Array.from(importDecrefs.keys()).sort(); + for (const baseRef of importVrefs) { + vrm.decRefCount(baseRef, importDecrefs.get(baseRef)); + } +} + +// 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 + // virtual objects (but not the root object). This will trigger + // refcount decrements which may drop some virtuals from the DB. It + // might also drop some objects from RAM. + + const abandonedVrefSet = new Set(); + identifyExportedRemotables(abandonedVrefSet, tools); + identifyExportedFacets(abandonedVrefSet, tools); + abandonExports(abandonedVrefSet, tools); + + // Then we pretend userspace RAM has dropped all the vref-based + // objects that it was holding onto. + finalizeEverything(tools); + + // Now we ask collectionManager for help with deleting all + // non-durable collections. This will delete all the DB entries + // (including the metadata), decref everything they point to, + // including imports and DOs, and add to possiblyDeadSet. There + // might still be a Presence for the collection in RAM, but if/when + // it is deleted, the collectionManager will tolerate (ignore) the + // resulting attempt to free all the entries a second time. + + tools.collectionManager.deleteAllVirtualCollections(); + + // Now we'll have finalizers pending and `possiblyDeadSet` will be + // populated with our simulated drops, so a `bringOutYourDead` will + // release a lot. + + // eslint-disable-next-line no-use-before-define + await tools.bringOutYourDead(); + + // possiblyDeadSet is now empty + + // NOTE: instead of using deleteAllVirtualCollections() above (which + // does a lot of decref work we don't really care about), we might + // use deleteCollectionsWithDecref() here, once it's ready. It + // should be faster because it doesn't need to care about refcounts + // of virtual objects or Remotables, only those of imports and + // durables. But it bypasses the usual refcounting code, so it + // should probably be called after the last BOYD. + // + // deleteCollectionsWithDecref(tools); + + // The remaining data is virtual objects which participate in cycles + // (although not through virtual collections, which were deleted + // above), durable objects held by [those virtual objects, durable + // object cycles, exports, or baggage], and imports held by all of + // those. + + // eslint-disable-next-line no-constant-condition + if (1) { + // We delete the data of all merely-virtual objects. For now, we + // don't attempt to decrement the refcounts of things they point + // to (which might allow us to drop some imports and a subset of + // the durable objects). + deleteVirtualObjectsWithoutDecref(tools); + + // The remaining data is durable objects which were held by + // virtual-object cycles, or are still held by durable-object + // cycles or exports or baggage, and imports held by all of those. + + // At this point we declare sufficient victory and return. + } else { + // We delete the data of all merely-virtual objects, and + // accumulate counts of the deleted references to durable objects + // and imports (ignoring references to Remotables and other + // virtual objects). After deletion, we apply the decrefs, which + // may cause some durable objects and imports to be added to + // possiblyDeadSet. + deleteVirtualObjectsWithDecref(tools); + + // possiblyDeadSet will now have baserefs for durable objects and + // imports (the ones that were only kept alive by virtual-object + // cycles). There won't be any virtual-object baserefs in + // possiblyDeadSet because we didn't apply any of those + // decrefs. So our `bringOutYourDead` won't try to read or modify + // any of the on-disk refcounts for VOs (which would fail because + // we deleted everything). + + // eslint-disable-next-line no-use-before-define + await tools.bringOutYourDead(); + + // The remaining data is durable objects which are held by a + // durable-object cycle, exports, or baggage, and imports held by + // all of those. + + // At this point we declare sufficient victory and return. + } +} diff --git a/packages/SwingSet/src/liveslots/virtualReferences.js b/packages/SwingSet/src/liveslots/virtualReferences.js index 53f500bb27c..e62d963f25d 100644 --- a/packages/SwingSet/src/liveslots/virtualReferences.js +++ b/packages/SwingSet/src/liveslots/virtualReferences.js @@ -617,6 +617,7 @@ export function makeVirtualReferenceManager( return harden({ droppedCollectionRegistry, isDurable, + isDurableKind, registerKind, rememberFacetNames, reanimate, diff --git a/packages/SwingSet/test/upgrade/bootstrap-upgrade.js b/packages/SwingSet/test/upgrade/bootstrap-upgrade.js index b32b9c08f72..d55e88b62f4 100644 --- a/packages/SwingSet/test/upgrade/bootstrap-upgrade.js +++ b/packages/SwingSet/test/upgrade/bootstrap-upgrade.js @@ -3,13 +3,38 @@ import { Far } from '@endo/marshal'; import { assert } from '@agoric/assert'; import { makePromiseKit } from '@endo/promise-kit'; +import { NUM_SENSORS } from './num-sensors.js'; + +function insistMissing(ref, isCollection = false) { + let p; + if (isCollection) { + p = E(ref).set(1, 2); + } else { + p = E(ref).get(); + } + return p.then( + () => { + throw Error('ref should be missing'); + }, + err => { + assert.equal(err.message, 'vat terminated'); + }, + ); +} + export function buildRootObject() { let vatAdmin; let ulrikRoot; let ulrikAdmin; const marker = Far('marker', {}); + // sensors are numbered from '1' to match the vobj o+N/1.. vrefs + const importSensors = ['skip0']; + for (let i = 1; i < NUM_SENSORS + 1; i += 1) { + importSensors.push(Far(`import-${i}`, {})); + } const { promise, resolve } = makePromiseKit(); let dur; + let retain; return Far('root', { async bootstrap(vats, devices) { @@ -20,6 +45,10 @@ export function buildRootObject() { return marker; }, + getImportSensors() { + return importSensors; + }, + async buildV1() { // build Ulrik, the upgrading vat const bcap = await E(vatAdmin).getNamedBundleCap('ulrik1'); @@ -34,17 +63,21 @@ export function buildRootObject() { const m2 = await E(ulrikRoot).getPresence(); assert.equal(m2, marker); const data = await E(ulrikRoot).getData(); - dur = await E(ulrikRoot).getDurandal('d1'); + + retain = await E(ulrikRoot).getExports(importSensors); + + dur = await E(ulrikRoot).getDurandal({ d1: 'd1' }); const d1arg = await E(dur).get(); - assert.equal(d1arg, 'd1'); + assert.equal(d1arg.d1, 'd1'); - // give v1 a promise that won't be resolved until v2 + // give ver1 a promise that won't be resolved until ver2 await E(ulrikRoot).acceptPromise(promise); const { p1 } = await E(ulrikRoot).getEternalPromise(); p1.catch(() => 'hush'); const p2 = E(ulrikRoot).returnEternalPromise(); // never resolves p2.catch(() => 'hush'); - return { version, data, p1, p2, ...parameters }; + + return { version, data, p1, p2, retain, ...parameters }; }, async upgradeV2() { @@ -56,10 +89,55 @@ export function buildRootObject() { const m2 = await E(ulrikRoot).getPresence(); assert.equal(m2, marker); const data = await E(ulrikRoot).getData(); + + // marshal splats a bunch of log messages when it serializes + // 'remoerr' at the end of this function, warn the human + console.log(`note: expect one 'vat terminated' error logged below`); + let remoerr; // = Error('foo'); + await E(retain.rem1) + .get() + .catch(err => (remoerr = err)); + const d1arg = await E(dur).get(); - assert.equal(d1arg, 'new d1'); // durable object still works, in new way + assert.equal(d1arg.d1, 'd1'); // durable object still works + assert.equal(d1arg.new, 'new'); // in the new way + + // the durables we retained should still be viable + const doget = obj => + E(obj) + .get() + .then(res => res.name); + assert.equal(await doget(retain.dur1), 'd1', 'retain.dur1 broken'); + + const dc4entries = await E(ulrikRoot).getEntries(retain.dc4); + assert.equal(dc4entries.length, 2); + const dur28 = await E(retain.dc4).get(importSensors[28]); + const imp28 = await E(dur28).getImport(); + assert.equal(imp28, importSensors[28], 'retain.dc4 broken'); + + // the durables retained by the vat in baggage should still be viable + const baggageImps = await E(ulrikRoot).checkBaggage( + importSensors[32], + importSensors[36], + ); + const { imp33, imp35, imp37, imp38 } = baggageImps; + assert.equal(imp33, importSensors[33]); + assert.equal(imp35, importSensors[35]); + assert.equal(imp37, importSensors[37]); + assert.equal(imp38, importSensors[38]); + + // all Remotable and merely-virtual objects are gone + await insistMissing(retain.vir2); + await insistMissing(retain.vir5); + await insistMissing(retain.vir7); + await insistMissing(retain.vc1, true); + await insistMissing(retain.vc3, true); + await insistMissing(retain.rem1); + await insistMissing(retain.rem2); + await insistMissing(retain.rem3); + resolve(`message for your predecessor, don't freak out`); - return { version, data, ...parameters }; + return { version, data, remoerr, ...parameters }; }, async buildV1WithLostKind() { diff --git a/packages/SwingSet/test/upgrade/num-sensors.js b/packages/SwingSet/test/upgrade/num-sensors.js new file mode 100644 index 00000000000..289eea593e8 --- /dev/null +++ b/packages/SwingSet/test/upgrade/num-sensors.js @@ -0,0 +1 @@ +export const NUM_SENSORS = 38; diff --git a/packages/SwingSet/test/upgrade/object-graph.pdf b/packages/SwingSet/test/upgrade/object-graph.pdf new file mode 100644 index 00000000000..72b3a55f96e Binary files /dev/null and b/packages/SwingSet/test/upgrade/object-graph.pdf differ diff --git a/packages/SwingSet/test/upgrade/test-upgrade.js b/packages/SwingSet/test/upgrade/test-upgrade.js index d903fab023a..e03d64ea3bc 100644 --- a/packages/SwingSet/test/upgrade/test-upgrade.js +++ b/packages/SwingSet/test/upgrade/test-upgrade.js @@ -4,10 +4,15 @@ import { test } from '../../tools/prepare-test-env-ava.js'; // eslint-disable-next-line import/order import { assert } from '@agoric/assert'; import { parse } from '@endo/marshal'; +import { getAllState } from '@agoric/swing-store'; import { provideHostStorage } from '../../src/controller/hostStorage.js'; +import { parseReachableAndVatSlot } from '../../src/kernel/state/reachable.js'; +import { parseVatSlot } from '../../src/lib/parseVatSlots.js'; import { initializeSwingset, makeSwingsetController } from '../../src/index.js'; import { capargs, capdataOneSlot } from '../util.js'; +import { NUM_SENSORS } from './num-sensors.js'; + function bfile(name) { return new URL(name, import.meta.url).pathname; } @@ -21,12 +26,42 @@ function get(capdata, propname) { return value; } +function getRetained(capdata, propname) { + const body = JSON.parse(capdata.body); + const value = body.retain[propname]; + if (typeof value === 'object' && value['@qclass'] === 'slot') { + return ['slot', capdata.slots[value.index]]; + } + return value; +} + +function getImportSensorKref(impcapdata, i) { + const body = JSON.parse(impcapdata.body); + const value = body[i]; + if (typeof value === 'object' && value['@qclass'] === 'slot') { + return ['slot', impcapdata.slots[value.index]]; + } + return value; +} + +// eslint-disable-next-line no-unused-vars +function dumpState(hostStorage, vatID) { + const s = getAllState(hostStorage).kvStuff; + const keys = Array.from(Object.keys(s)).sort(); + for (const k of keys) { + if (k.startsWith(`${vatID}.vs.`)) { + console.log(k, s[k]); + } + } +} + async function testUpgrade(t, defaultManagerType) { const config = { includeDevDependencies: true, // for vat-data defaultManagerType, bootstrap: 'bootstrap', - defaultReapInterval: 'never', + // defaultReapInterval: 'never', + // defaultReapInterval: 1, vats: { bootstrap: { sourceSpec: bfile('bootstrap-upgrade.js') }, }, @@ -37,8 +72,13 @@ async function testUpgrade(t, defaultManagerType) { }; const hostStorage = provideHostStorage(); + const { kvStore } = hostStorage; await initializeSwingset(config, [], hostStorage); - const c = await makeSwingsetController(hostStorage); + const c = await makeSwingsetController( + hostStorage, + {}, + { slogFile: 's.slog' }, + ); c.pinVatRoot('bootstrap'); await c.run(); @@ -56,6 +96,16 @@ async function testUpgrade(t, defaultManagerType) { const markerKref = mcd[1].slots[0]; // probably ko26 t.deepEqual(mcd[1], capdataOneSlot(markerKref, 'marker')); + // fetch all the "importSensors": exported by bootstrap, imported by + // the upgraded vat. We'll determine their krefs and later query the + // upgraded vat to see if it's still importing them or not + const [impstatus, impcapdata] = await run('getImportSensors', []); + t.is(impstatus, 'fulfilled'); + const impKrefs = ['skip0']; + for (let i = 1; i < NUM_SENSORS + 1; i += 1) { + impKrefs.push(getImportSensorKref(impcapdata, i)[1]); + } + // create initial version const [v1status, v1capdata] = await run('buildV1', []); t.is(v1status, 'fulfilled'); @@ -69,19 +119,144 @@ async function testUpgrade(t, defaultManagerType) { t.is(get(v1capdata, 'p2')[0], 'slot'); const v1p2Kref = get(v1capdata, 'p2')[1]; - // upgrade should work + // grab exports to deduce durable/virtual vrefs + t.is(getRetained(v1capdata, 'dur1')[0], 'slot'); + const dur1Kref = getRetained(v1capdata, 'dur1')[1]; + t.is(getRetained(v1capdata, 'vir2')[0], 'slot'); + const vir2Kref = getRetained(v1capdata, 'vir2')[1]; + const vir5Kref = getRetained(v1capdata, 'vir5')[1]; + const vir7Kref = getRetained(v1capdata, 'vir7')[1]; + + const vatID = kvStore.get(`${dur1Kref}.owner`); // probably v6 + function getVref(kref) { + const s = kvStore.get(`${vatID}.c.${kref}`); + return parseReachableAndVatSlot(s).vatSlot; + } + function krefReachable(kref) { + const s = kvStore.get(`${vatID}.c.${kref}`); + return !!(s && parseReachableAndVatSlot(s).isReachable); + } + // We look in the vat's vatstore to see if the virtual/durable + // object exists or not (as a state record). + function vomHas(vref) { + return kvStore.has(`${vatID}.vs.vom.${vref}`); + } + + // dumpState(hostStorage, vatID); + + // deduce exporter vrefs for all durable/virtual objects, and assert + // that they're still in DB + const dur1Vref = getVref(dur1Kref); + t.is(parseVatSlot(dur1Vref).subid, 1n); + const durBase = dur1Vref.slice(0, dur1Vref.length - 2); + function durVref(i) { + return `${durBase}/${i}`; + } + const vir2Vref = getVref(vir2Kref); + t.is(parseVatSlot(vir2Vref).subid, 2n); + const virBase = vir2Vref.slice(0, vir2Vref.length - 2); + function virVref(i) { + return `${virBase}/${i}`; + } + + t.true(vomHas(durVref(1))); + t.true(vomHas(virVref(2))); + t.false(vomHas(virVref(1))); // deleted before upgrade + t.false(vomHas(durVref(2))); // deleted before upgrade + + // remember krefs for the exported objects so we can check their + // abandonment + const retainedNames = 'dur1 vir2 vir5 vir7 vc1 vc3 dc4 rem1 rem2 rem3'; + const retainedKrefs = {}; + for (const name of retainedNames.split(' ')) { + const d = getRetained(v1capdata, name); + t.is(d[0], 'slot'); + retainedKrefs[name] = d[1]; + } + + // now perform the upgrade + // console.log(`-- starting upgradeV2`); + const [v2status, v2capdata] = await run('upgradeV2', []); t.is(v2status, 'fulfilled'); t.deepEqual(get(v2capdata, 'version'), 'v2'); t.deepEqual(get(v2capdata, 'youAre'), 'v2'); t.deepEqual(get(v2capdata, 'marker'), ['slot', markerKref]); t.deepEqual(get(v2capdata, 'data'), ['some', 'data']); + const remoerr = parse(JSON.stringify(get(v2capdata, 'remoerr'))); + t.deepEqual(remoerr, Error('vat terminated')); // the old version's non-durable promises should be rejected t.is(c.kpStatus(v1p1Kref), 'rejected'); t.deepEqual(c.kpResolution(v1p1Kref), capargs('vat upgraded')); t.is(c.kpStatus(v1p2Kref), 'rejected'); t.deepEqual(c.kpResolution(v1p2Kref), capargs('vat upgraded')); + + // dumpState(hostStorage, vatID); + + // all the merely-virtual exports should be gone + for (let i = 1; i < NUM_SENSORS + 1; i += 1) { + t.false(vomHas(virVref(i))); + } + + // of the durables, only these survive + const survivingDurables = [ + 1, 16, 17, 18, 19, 20, 26, 27, 28, 33, 34, 35, 36, 37, + ]; + // and these imports (imp38 is held by baggage) + const survivingImported = [ + 1, 16, 17, 18, 19, 20, 26, 27, 28, 33, 34, 35, 36, 37, 38, + ]; + + // but implementation limitations/bugs cause the following unwanted + // effects (these adjustments should be deleted as we fix them): + + // stopVat() uses deleteVirtualObjectsWithoutDecref, rather than + // deleteVirtualObjectsWithDecref, which means lingering virtual + // objects (i.e. cycles) don't drop their referenced objects as we + // delete them + survivingDurables.push(9); + survivingImported.push(7); + survivingImported.push(8); + survivingImported.push(9); + + // When a virtual collection is deleted, the loop that deletes all + // entries will re-instantiate all the keys, but doesn't set + // doMoreGC, so processDeadSet doesn't redo the gcAndFinalize, and + // the virtual object cache is probably still holding onto the new + // Representative anyways. This retains the durables that were held + // by deleted collections (dur10/dur13/dur23, depending on the cache + // size, just dur23 if size=0) and the imports they hold. Bug #5053 + // is about fixing clearInternal to avoid this, when that's fixed + // these should be removed. + survivingDurables.push(10); + survivingImported.push(10); + survivingDurables.push(13); + survivingImported.push(13); + survivingDurables.push(23); + survivingImported.push(23); + + for (let i = 1; i < NUM_SENSORS + 1; i += 1) { + const vref = durVref(i); + const impKref = impKrefs[i]; + const expD = survivingDurables.includes(i); + const expI = survivingImported.includes(i); + const reachable = krefReachable(impKref); + t.is(vomHas(vref), expD, `dur[${i}] not ${expD}`); + t.is(reachable, expI, `imp[${i}] not ${expI}`); + // const abb = (b) => b.toString().slice(0,1).toUpperCase(); + // const vomS = `vom: ${abb(expD)} ${abb(vomHas(vref))}`; + // const reachS = `${abb(expI)} ${abb(reachable)}`; + // const match = (expD === vomHas(vref)) && (expI === reachable); + // const matchS = `${match ? 'true' : 'FALSE'}`; + // const s = kvStore.get(`${vatID}.c.${impKref}`); + // console.log(`${i}: ${vomS} imp: ${reachS} ${matchS} ${impKref} ${s}`); + } + + // check koNN.owner to confirm the exported virtuals (2/5/7) are abandoned + t.false(kvStore.has(`${vir2Kref}.owner`)); + t.false(kvStore.has(`${vir5Kref}.owner`)); + t.false(kvStore.has(`${vir7Kref}.owner`)); } test('vat upgrade - local', async t => { @@ -127,6 +302,8 @@ test('failed upgrade - lost kind', async t => { t.is(v1status, 'fulfilled'); // upgrade should fail + console.log(`note: expect a 'defineDurableKind not called' error below`); + console.log(`also: 'vat-upgrade failure notification not implemented'`); const [v2status, v2capdata] = await run('upgradeV2WhichLosesKind', []); t.is(v2status, 'rejected'); console.log(v2capdata); diff --git a/packages/SwingSet/test/upgrade/vat-ulrik-1.js b/packages/SwingSet/test/upgrade/vat-ulrik-1.js index 52a4b6f7994..f7fc6fadffe 100644 --- a/packages/SwingSet/test/upgrade/vat-ulrik-1.js +++ b/packages/SwingSet/test/upgrade/vat-ulrik-1.js @@ -1,28 +1,139 @@ -/* global VatData */ import { Far } from '@endo/marshal'; import { makePromiseKit } from '@endo/promise-kit'; -// import { makeKindHandle, defineDurableKind } from '@agoric/vat-data'; -const { makeKindHandle, defineDurableKind } = VatData; +import { + makeKindHandle, + defineDurableKind, + defineKind, + makeScalarBigMapStore, + makeScalarBigWeakMapStore, +} from '@agoric/vat-data'; +import { NUM_SENSORS } from './num-sensors.js'; const durandalHandle = makeKindHandle('durandal'); -function initializeDurandal(arg) { - return { arg }; +function initialize(name, imp, value) { + return harden({ name, imp, value }); } -const durandalBehavior = { +const behavior = { get({ state }) { - return state.arg; + return state.value; }, - set({ state }, arg) { - state.arg = arg; + set({ state }, value) { + state.value = value; }, }; -const makeDurandal = defineDurableKind( - durandalHandle, - initializeDurandal, - durandalBehavior, -); +const makeDurandal = defineDurableKind(durandalHandle, initialize, behavior); +const makeVir = defineKind('virtual', initialize, behavior); +const makeDummy = defineKind('dummy', initialize, behavior); + +// TODO: explore 'export modRetains' +// eslint-disable-next-line no-unused-vars +let modRetains; + +// we set up a lot of virtual and durable objects to test what gets +// deleted vs retained (see object-graph.pdf for the test plan) + +function makeRemotable(name, held) { + return Far(name, { get: () => held }); +} + +function buildExports(baggage, imp) { + // each virtual/durable object has a unique import, some of which + // should be dropped during upgrade + + // start these at '1' to match the vrefs (o+10/1 .. /5) for debugging + const vir = ['skip0']; + const dur = ['skip0']; + for (let i = 1; i < NUM_SENSORS + 1; i += 1) { + vir.push(makeVir(`v${i}`, imp[i], { name: `v${i}` })); + dur.push(makeDurandal(`d${i}`, imp[i], { name: `d${i}` })); + } + + // vc1+vc2 form a cycle, as do vir[7]+vir[8], and our lack of + // cycle-collection means we don't GC it during the lifetime of the + // vat, however they'll be deleted during upgrade because stopVat() + // deletes all virtual data independent of refcounts + + const vc1 = makeScalarBigMapStore('vc1'); + const vc2 = makeScalarBigMapStore('vc2'); + const vc3 = makeScalarBigMapStore('vc3'); + + // dc1+dc2 form an unreferenced cycle, as do dur[16]+dur[17], which + // stick around (even after upgrade) because we don't yet have + // cycle-collecting virtual/durable-object GC. dc3 is dropped (only + // held by an abandoned Remotable), dc4 is retained by an export, + // dc5+dc6 are retained by baggage + + const dc1 = makeScalarBigMapStore('dc1', { durable: true }); + const dc2 = makeScalarBigMapStore('dc2', { durable: true }); + const dc3 = makeScalarBigMapStore('dc3', { durable: true }); + const dc4 = makeScalarBigMapStore('dc4', { durable: true }); + const dc5 = makeScalarBigWeakMapStore('dc5', { durable: true }); + const dc6 = makeScalarBigMapStore('dc6', { durable: true }); + + // these Remotables are both exported and held by module-level pins, + // but will still be abandoned + const rem1 = makeRemotable('rem1', [imp[4], vir[5], vir[7], vc1, vc3]); + const rem2 = makeRemotable('rem2', [dur[21], dc3]); + const rem3 = makeRemotable('rem3', [dur[29], imp[30], vir[31]]); + modRetains = { rem1, rem2, rem3 }; + + // now wire them up according to the diagram + vir[2].set(dur[3]); + vir[5].set(dur[6]); + vir[7].set(vir[8]); + vir[8].set(harden([vir[7], dur[9]])); + vc1.init('vc2', vc2); + vc2.init('vc1', vc1); + vc2.init(dur[10], dur[11]); + vc2.init(imp[12], dur[12]); + vc3.init(dur[13], dur[14]); + vc3.init(imp[15], dur[15]); + dur[16].set(dur[17]); + dur[17].set(dur[16]); + dc1.init('dc2', dc2); + dc2.init('dc1', dc1); + dc2.init(dur[18], dur[19]); + dc2.init(imp[20], dur[20]); + dur[21].set(dur[22]); + dc3.init(dur[23], dur[24]); + dc3.init(imp[25], dur[25]); + dc4.init(dur[26], dur[27]); + dc4.init(imp[28], dur[28]); + dc5.init(imp[32], dur[33]); // imp[32] exported+held by bootstrap + dc6.init(dur[34], dur[35]); + dc6.init(imp[36], dur[36]); + baggage.init('dc5', dc5); + baggage.init('dc6', dc6); + baggage.init('dur37', dur[37]); + baggage.init('imp38', imp[38]); + + // We set virtualObjectCacheSize=0 to ensure all data writes are + // made promptly, But the cache will still retain the last + // Representative, which inhibits GC. So the last thing we do here + // should be to create/deserialize a throwaway object, to make sure + // all the ones we're measuring are collected as we expect. + + makeDummy(); // knock the last dur/vir/vc/dc out of the cache + + // we share dur1/vir2 with the test harness so it can glean the + // baserefs and interpolate the full vrefs for everything else + // without holding a GC pin on them + + return { + dur1: dur[1], + vir2: vir[2], + vir5: vir[5], + vir7: vir[7], + vc1, + vc3, + dc4, + rem1, + rem2, + rem3, + }; +} export function buildRootObject(_vatPowers, vatParameters, baggage) { const { promise: p1 } = makePromiseKit(); @@ -50,7 +161,10 @@ export function buildRootObject(_vatPowers, vatParameters, baggage) { return baggage.get('data'); }, getDurandal(arg) { - return makeDurandal(arg); + return makeDurandal('durandal', 0, arg); + }, + getExports(imp) { + return buildExports(baggage, imp); }, acceptPromise(p) { diff --git a/packages/SwingSet/test/upgrade/vat-ulrik-2.js b/packages/SwingSet/test/upgrade/vat-ulrik-2.js index 5ac945ba235..1e471defee9 100644 --- a/packages/SwingSet/test/upgrade/vat-ulrik-2.js +++ b/packages/SwingSet/test/upgrade/vat-ulrik-2.js @@ -1,24 +1,26 @@ -/* global VatData */ import { Far } from '@endo/marshal'; -// import { defineDurableKind } from '@agoric/vat-data'; -const { defineDurableKind } = VatData; +import { assert } from '@agoric/assert'; +import { defineDurableKind } from '@agoric/vat-data'; -function initializeDurandal(arg) { - return { arg }; +function initialize(name, imp, value) { + return harden({ name, imp, value }); } -const durandalBehavior = { +const behavior = { get({ state }) { - return `new ${state.arg}`; + return { new: 'new', ...state.value }; }, - set({ state }, arg) { - state.arg = arg; + getImport({ state }) { + return state.imp; + }, + set({ state }, value) { + state.value = value; }, }; export function buildRootObject(_vatPowers, vatParameters, baggage) { const durandalHandle = baggage.get('durandalHandle'); - defineDurableKind(durandalHandle, initializeDurandal, durandalBehavior); + defineDurableKind(durandalHandle, initialize, behavior); return Far('root', { getVersion() { @@ -33,5 +35,25 @@ export function buildRootObject(_vatPowers, vatParameters, baggage) { getData() { return baggage.get('data'); }, + getEntries(collection) { + return Array.from(collection.entries()); + }, + checkBaggage(imp32, imp36) { + // console.log(`baggage:`, Array.from(baggage.keys())); + const dc5 = baggage.get('dc5'); + const dc6 = baggage.get('dc6'); + const imp33 = dc5.get(imp32).getImport(); + let dur34; + for (const key of dc6.keys()) { + if (key !== imp36) { + dur34 = key; + } + } + const imp35 = dc6.get(dur34).getImport(); + assert.equal(imp36, dc6.get(imp36).getImport()); + const imp37 = baggage.get('dur37').getImport(); + const imp38 = baggage.get('imp38'); + return { imp33, imp35, imp37, imp38 }; + }, }); }