diff --git a/packages/SwingSet/src/controller/controller.js b/packages/SwingSet/src/controller/controller.js index 89bef75a7d3..01e01dcba83 100644 --- a/packages/SwingSet/src/controller/controller.js +++ b/packages/SwingSet/src/controller/controller.js @@ -115,21 +115,23 @@ export function makeStartXSnap(bundles, { snapStore, env, spawn }) { } /** + * @param {string} vatID * @param {string} name * @param {(request: Uint8Array) => Promise} handleCommand * @param {boolean} [metered] - * @param {string} [snapshotHash] + * @param {boolean} [reload] */ async function startXSnap( + vatID, name, handleCommand, metered, - snapshotHash = undefined, + reload = false, ) { const meterOpts = metered ? {} : { meteringLimit: 0 }; - if (snapStore && snapshotHash) { + if (snapStore && reload) { // console.log('startXSnap from', { snapshotHash }); - return snapStore.load(snapshotHash, async snapshot => { + return snapStore.loadSnapshot(vatID, async snapshot => { const xs = doXSnap({ snapshot, name, diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index eabf0f3b6f3..11cc99cd336 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -89,8 +89,7 @@ const enableKernelGC = true; // v$NN.reapInterval = $NN or 'never' // v$NN.reapCountdown = $NN or 'never' // exclude from consensus -// local.v$NN.lastSnapshot = JSON({ snapshotID, startPos }) -// local.snapshot.$id = [vatID, ...] +// local.* // m$NN.remaining = $NN // remaining capacity (in computrons) or 'unlimited' // m$NN.threshold = $NN // notify when .remaining first drops below this @@ -786,10 +785,7 @@ export default function makeKernelKeeper(kernelStorage, kernelSlog) { const promisePrefix = `${vatID}.c.p`; const kernelPromisesToReject = []; - const old = vatKeeper.getLastSnapshot(); - if (old) { - vatKeeper.removeFromSnapshot(old.snapshotID); - } + vatKeeper.deleteSnapshots(); // Note: ASCII order is "+,-./", and we rely upon this to split the // keyspace into the various o+NN/o-NN/etc spaces. If we were using a diff --git a/packages/SwingSet/src/kernel/state/vatKeeper.js b/packages/SwingSet/src/kernel/state/vatKeeper.js index ce20026bdfc..0e14ac33b0b 100644 --- a/packages/SwingSet/src/kernel/state/vatKeeper.js +++ b/packages/SwingSet/src/kernel/state/vatKeeper.js @@ -45,14 +45,8 @@ export function initializeVatState(kvStore, streamStore, vatID) { kvStore.set(`${vatID}.d.nextID`, `${FIRST_DEVICE_ID}`); kvStore.set(`${vatID}.nextDeliveryNum`, `0`); kvStore.set(`${vatID}.incarnationNumber`, `1`); - kvStore.set( - `${vatID}.t.startPosition`, - `${JSON.stringify(streamStore.STREAM_START)}`, - ); - kvStore.set( - `${vatID}.t.endPosition`, - `${JSON.stringify(streamStore.STREAM_START)}`, - ); + kvStore.set(`${vatID}.t.startPosition`, `${streamStore.STREAM_START}`); + kvStore.set(`${vatID}.t.endPosition`, `${streamStore.STREAM_START}`); } /** @@ -486,9 +480,9 @@ export function makeVatKeeper( */ function* getTranscript(startPos) { if (startPos === undefined) { - startPos = JSON.parse(getRequired(`${vatID}.t.startPosition`)); + startPos = Number(getRequired(`${vatID}.t.startPosition`)); } - const endPos = JSON.parse(getRequired(`${vatID}.t.endPosition`)); + const endPos = Number(getRequired(`${vatID}.t.endPosition`)); for (const entry of streamStore.readStream( transcriptStream, /** @type { StreamPosition } */ (startPos), @@ -504,88 +498,34 @@ export function makeVatKeeper( * @param {object} entry The transcript entry to append. */ function addToTranscript(entry) { - const oldPos = JSON.parse(getRequired(`${vatID}.t.endPosition`)); + const oldPos = Number(getRequired(`${vatID}.t.endPosition`)); const newPos = streamStore.writeStreamItem( transcriptStream, JSON.stringify(entry), oldPos, ); - kvStore.set(`${vatID}.t.endPosition`, `${JSON.stringify(newPos)}`); + kvStore.set(`${vatID}.t.endPosition`, `${newPos}`); } /** @returns {StreamPosition} */ function getTranscriptEndPosition() { - return JSON.parse( + const endPosition = kvStore.get(`${vatID}.t.endPosition`) || - assert.fail('missing endPosition'), - ); + assert.fail('missing endPosition'); + return Number(endPosition); } - /** - * @returns {{ snapshotID: string, startPos: StreamPosition } | undefined} - */ - function getLastSnapshot() { - const notation = kvStore.get(`local.${vatID}.lastSnapshot`); - if (!notation) { - return undefined; - } - const { snapshotID, startPos } = JSON.parse(notation); - assert.typeof(snapshotID, 'string'); - assert(startPos); - return { snapshotID, startPos }; + function getSnapshotInfo() { + return snapStore?.getSnapshotInfo(vatID); } function transcriptSnapshotStats() { const totalEntries = getTranscriptEndPosition(); - const lastSnapshot = getLastSnapshot(); - const snapshottedEntries = lastSnapshot ? lastSnapshot.startPos : 0; + const snapshotInfo = getSnapshotInfo(); + const snapshottedEntries = snapshotInfo ? snapshotInfo.endPos : 0; return { totalEntries, snapshottedEntries }; } - /** - * Add vatID to consumers of a snapshot. - * - * @param {string} snapshotID - */ - function addToSnapshot(snapshotID) { - const key = `local.snapshot.${snapshotID}`; - const consumers = JSON.parse(kvStore.get(key) || '[]'); - assert(Array.isArray(consumers)); - - // We can't completely rule out the possibility that - // a vat will use the same snapshot twice in a row. - // - // PERFORMANCE NOTE: we assume consumer lists are short; - // usually length 1. So O(n) search here is better - // than keeping the list sorted. - if (!consumers.includes(vatID)) { - consumers.push(vatID); - kvStore.set(key, JSON.stringify(consumers)); - // console.log('addToSnapshot result:', { vatID, snapshotID, consumers }); - } - } - - /** - * Remove vatID from consumers of a snapshot. - * - * @param {string} snapshotID - */ - function removeFromSnapshot(snapshotID) { - const key = `local.snapshot.${snapshotID}`; - const consumersJSON = kvStore.get(key); - if (!consumersJSON) { - throw Fail`cannot remove ${vatID}: ${key} key not defined`; - } - const consumers = JSON.parse(consumersJSON); - assert(Array.isArray(consumers)); - const ix = consumers.indexOf(vatID); - assert(ix >= 0); - consumers.splice(ix, 1); - // console.log('removeFromSnapshot done:', { vatID, snapshotID, consumers }); - kvStore.set(key, JSON.stringify(consumers)); - return consumers.length; - } - /** * Store a snapshot, if given a snapStore. * @@ -597,30 +537,19 @@ export function makeVatKeeper( return false; } - const info = await manager.makeSnapshot(snapStore); + const endPosition = getTranscriptEndPosition(); + const info = await manager.makeSnapshot(endPosition, snapStore); const { - hash: snapshotID, + hash, rawByteCount, rawSaveSeconds, compressedByteCount, compressSeconds, } = info; - const old = getLastSnapshot(); - if (old && old.snapshotID !== snapshotID) { - if (removeFromSnapshot(old.snapshotID) === 0) { - snapStore.deleteSnapshot(old.snapshotID); - } - } - const endPosition = getTranscriptEndPosition(); - kvStore.set( - `local.${vatID}.lastSnapshot`, - JSON.stringify({ snapshotID, startPos: endPosition }), - ); - addToSnapshot(snapshotID); kernelSlog.write({ type: 'heap-snapshot-save', vatID, - snapshotID, + hash, rawByteCount, rawSaveSeconds, compressedByteCount, @@ -630,17 +559,15 @@ export function makeVatKeeper( return true; } + function deleteSnapshots() { + if (snapStore) { + snapStore.deleteVatSnapshots(vatID); + } + } + function removeSnapshotAndTranscript() { - const skey = `local.${vatID}.lastSnapshot`; if (snapStore) { - const notation = kvStore.get(skey); - if (notation) { - const { snapshotID } = JSON.parse(notation); - if (removeFromSnapshot(snapshotID) === 0) { - snapStore.deleteSnapshot(snapshotID); - } - kvStore.delete(skey); - } + snapStore.deleteVatSnapshots(vatID); } const endPos = getRequired(`${vatID}.t.endPosition`); @@ -719,8 +646,8 @@ export function makeVatKeeper( vatStats, dumpState, saveSnapshot, - getLastSnapshot, - removeFromSnapshot, + deleteSnapshots, + getSnapshotInfo, removeSnapshotAndTranscript, }); } diff --git a/packages/SwingSet/src/kernel/vat-loader/manager-helper.js b/packages/SwingSet/src/kernel/vat-loader/manager-helper.js index f66e835d114..82b811eed89 100644 --- a/packages/SwingSet/src/kernel/vat-loader/manager-helper.js +++ b/packages/SwingSet/src/kernel/vat-loader/manager-helper.js @@ -47,7 +47,7 @@ import { makeTranscriptManager } from './transcript.js'; /** * * @typedef { { getManager: (shutdown: () => Promise, - * makeSnapshot?: (ss: SnapStore) => Promise) => VatManager, + * makeSnapshot?: (endPos: number, ss: SnapStore) => Promise) => VatManager, * syscallFromWorker: (vso: VatSyscallObject) => VatSyscallResult, * setDeliverToWorker: (dtw: unknown) => void, * } } ManagerKit @@ -259,7 +259,7 @@ function makeManagerKit( /** * * @param { () => Promise} shutdown - * @param {(ss: SnapStore) => Promise} makeSnapshot + * @param {(endPos: number, ss: SnapStore) => Promise} makeSnapshot * @returns {VatManager} */ function getManager(shutdown, makeSnapshot) { diff --git a/packages/SwingSet/src/kernel/vat-loader/manager-subprocess-xsnap.js b/packages/SwingSet/src/kernel/vat-loader/manager-subprocess-xsnap.js index f05586ffd05..b0f20aaa14f 100644 --- a/packages/SwingSet/src/kernel/vat-loader/manager-subprocess-xsnap.js +++ b/packages/SwingSet/src/kernel/vat-loader/manager-subprocess-xsnap.js @@ -22,7 +22,7 @@ const decoder = new TextDecoder(); * allVatPowers: VatPowers, * kernelKeeper: KernelKeeper, * kernelSlog: KernelSlog, - * startXSnap: (name: string, handleCommand: AsyncHandler, metered?: boolean, snapshotHash?: string) => Promise, + * startXSnap: (vatID: string, name: string, handleCommand: AsyncHandler, metered?: boolean, reload?: boolean) => Promise, * testLog: (...args: unknown[]) => void, * }} tools * @returns {VatManagerFactory} @@ -112,10 +112,9 @@ export function makeXsSubprocessFactory({ } const vatKeeper = kernelKeeper.provideVatKeeper(vatID); - const lastSnapshot = vatKeeper.getLastSnapshot(); - if (lastSnapshot) { - const { snapshotID } = lastSnapshot; - kernelSlog.write({ type: 'heap-snapshot-load', vatID, snapshotID }); + const snapshotInfo = vatKeeper.getSnapshotInfo(); + if (snapshotInfo) { + kernelSlog.write({ type: 'heap-snapshot-load', vatID, ...snapshotInfo }); } // `startXSnap` adds `argName` as a dummy argument so that 'ps' @@ -125,10 +124,11 @@ export function makeXsSubprocessFactory({ // start the worker and establish a connection const worker = await startXSnap( + vatID, argName, handleCommand, metered, - lastSnapshot ? lastSnapshot.snapshotID : undefined, + !!snapshotInfo, ); /** @type { (item: Tagged) => Promise } */ @@ -144,7 +144,7 @@ export function makeXsSubprocessFactory({ return { ...result, reply: [tag, ...rest] }; } - if (lastSnapshot) { + if (snapshotInfo) { parentLog(vatID, `snapshot loaded. dispatch ready.`); } else { parentLog(vatID, `instructing worker to load bundle..`); @@ -209,11 +209,12 @@ export function makeXsSubprocessFactory({ return worker.close().then(_ => undefined); } /** + * @param {number} endPos * @param {SnapStore} snapStore - * @returns {Promise} + * @returns {Promise} */ - function makeSnapshot(snapStore) { - return snapStore.save(fn => worker.snapshot(fn)); + function makeSnapshot(endPos, snapStore) { + return snapStore.saveSnapshot(vatID, endPos, fn => worker.snapshot(fn)); } return mk.getManager(shutdown, makeSnapshot); diff --git a/packages/SwingSet/src/kernel/vat-warehouse.js b/packages/SwingSet/src/kernel/vat-warehouse.js index 622f18e1089..bd0dc911a1c 100644 --- a/packages/SwingSet/src/kernel/vat-warehouse.js +++ b/packages/SwingSet/src/kernel/vat-warehouse.js @@ -125,9 +125,9 @@ export function makeVatWarehouse(kernelKeeper, vatLoader, policyOptions) { // TODO(3218): persist this option; avoid spinning up a vat that isn't pipelined const { enablePipelining = false } = options; - const lastSnapshot = vatKeeper.getLastSnapshot(); + const snapshotInfo = vatKeeper.getSnapshotInfo(); await manager.replayTranscript( - lastSnapshot ? lastSnapshot.startPos : undefined, + snapshotInfo ? snapshotInfo.endPos : undefined, ); const result = { diff --git a/packages/SwingSet/src/types-ambient.js b/packages/SwingSet/src/types-ambient.js index 49e37a256a2..227226a6628 100644 --- a/packages/SwingSet/src/types-ambient.js +++ b/packages/SwingSet/src/types-ambient.js @@ -118,7 +118,7 @@ * @typedef { import('./types-external.js').VatManagerFactory } VatManagerFactory * @typedef { import('./types-external.js').VatManager } VatManager * @typedef { import('./types-external.js').SnapStore } SnapStore - * @typedef { import('./types-external.js').SnapshotInfo } SnapshotInfo + * @typedef { import('./types-external.js').SnapshotResult } SnapshotResult * @typedef { import('./types-external.js').WaitUntilQuiescent } WaitUntilQuiescent */ diff --git a/packages/SwingSet/src/types-external.js b/packages/SwingSet/src/types-external.js index e69677ba33a..a39aa729dfb 100644 --- a/packages/SwingSet/src/types-external.js +++ b/packages/SwingSet/src/types-external.js @@ -221,7 +221,7 @@ export {}; * } } VatManagerFactory * @typedef { { deliver: (delivery: VatDeliveryObject) => Promise, * replayTranscript: (startPos: StreamPosition | undefined) => Promise, - * makeSnapshot?: (ss: SnapStore) => Promise, + * makeSnapshot?: (endPos: number, ss: SnapStore) => Promise, * shutdown: () => Promise, * } } VatManager * @@ -291,7 +291,7 @@ export {}; /** * @typedef { import('@agoric/swing-store').KVStore } KVStore * @typedef { import('@agoric/swing-store').SnapStore } SnapStore - * @typedef { import('@agoric/swing-store').SnapshotInfo } SnapshotInfo + * @typedef { import('@agoric/swing-store').SnapshotResult } SnapshotResult * @typedef { import('@agoric/swing-store').StreamStore } StreamStore * @typedef { import('@agoric/swing-store').StreamPosition } StreamPosition * @typedef { import('@agoric/swing-store').SwingStore } SwingStore diff --git a/packages/SwingSet/test/test-state.js b/packages/SwingSet/test/test-state.js index 5479bdda514..d09104a4c86 100644 --- a/packages/SwingSet/test/test-state.js +++ b/packages/SwingSet/test/test-state.js @@ -697,11 +697,7 @@ test('crankhash - skip keys', t => { // certain local keys are excluded from consensus, and should not affect // the hash k.kvStore.set('one', '1'); - k.kvStore.set('local.snapshot.XYZ', '["vat1234"]'); - k.kvStore.set( - 'local.v1234.lastSnapshot', - '{"snapshotID":"XYZ","startPos":4}', - ); + k.kvStore.set('local.doNotHashMe', 'random nonsense'); t.throws(() => k.kvStore.set('host.foo', 'bar')); t.is(k.emitCrankHashes().crankhash, expCrankhash); }); diff --git a/packages/SwingSet/test/test-xsnap-errors.js b/packages/SwingSet/test/test-xsnap-errors.js index 59e151c6f7b..aa4436603a5 100644 --- a/packages/SwingSet/test/test-xsnap-errors.js +++ b/packages/SwingSet/test/test-xsnap-errors.js @@ -38,7 +38,7 @@ test('child termination distinguished from meter exhaustion', async t => { /** @type { any } */ const kernelKeeper = { provideVatKeeper: () => ({ - getLastSnapshot: () => undefined, + getSnapshotInfo: () => undefined, addToTranscript: () => undefined, }), getRelaxDurabilityRules: () => false, @@ -60,6 +60,7 @@ test('child termination distinguished from meter exhaustion', async t => { // @ts-expect-error close enough for this test const managerOptions = { useTranscript: true }; const schandler = _vso => ['ok', null]; + const m = await xsWorkerFactory.createFromBundle( 'v1', bundle, diff --git a/packages/SwingSet/test/test-xsnap-metering.js b/packages/SwingSet/test/test-xsnap-metering.js index 976449b145e..36bbc38175c 100644 --- a/packages/SwingSet/test/test-xsnap-metering.js +++ b/packages/SwingSet/test/test-xsnap-metering.js @@ -48,19 +48,18 @@ async function doTest(t, metered) { const store = makeSnapStore(db, makeSnapStoreIO()); const { p: p1, startXSnap: start1 } = make(store); - let snapshotHash; - const worker1 = await start1('name', handleCommand, metered, snapshotHash); + const worker1 = await start1('vat', 'name', handleCommand, metered, false); const spawnArgs1 = await p1; checkMetered(t, spawnArgs1, metered); await worker1.evaluate('1+2'); t.teardown(() => worker1.close()); // now extract a snapshot - ({ hash: snapshotHash } = await store.save(worker1.snapshot)); + await store.saveSnapshot('vat', 1, worker1.snapshot); // and load it into a new worker const { p: p2, startXSnap: start2 } = make(store); - const worker2 = await start2('name', handleCommand, metered, snapshotHash); + const worker2 = await start2('vat', 'name', handleCommand, metered, true); const spawnArgs2 = await p2; checkMetered(t, spawnArgs2, metered); await worker2.evaluate('1+2'); diff --git a/packages/SwingSet/test/test-xsnap-store.js b/packages/SwingSet/test/test-xsnap-store.js index 1cf6eaeb7a9..300924d1b4f 100644 --- a/packages/SwingSet/test/test-xsnap-store.js +++ b/packages/SwingSet/test/test-xsnap-store.js @@ -79,9 +79,13 @@ test(`create XS Machine, snapshot (${snapSize.raw} Kb), compress to smaller`, as const db = sqlite3(':memory:'); const store = makeSnapStore(db, makeMockSnapStoreIO()); - const { compressedByteCount } = await store.save(async snapFile => { - await vat.snapshot(snapFile); - }); + const { compressedByteCount } = await store.saveSnapshot( + 'vat0', + 1, + async snapFile => { + await vat.snapshot(snapFile); + }, + ); t.true( relativeSize(compressedByteCount, snapSize.raw) < 0.5, @@ -98,9 +102,13 @@ test('SES bootstrap, save, compress', async t => { await vat.evaluate('globalThis.x = harden({a: 1})'); - const { compressedByteCount } = await store.save(async snapFile => { - await vat.snapshot(snapFile); - }); + const { compressedByteCount } = await store.saveSnapshot( + 'vat0', + 1, + async snapFile => { + await vat.snapshot(snapFile); + }, + ); t.true( relativeSize(compressedByteCount, snapSize.SESboot) < 0.5, @@ -115,9 +123,9 @@ test('create SES worker, save, restore, resume', async t => { const vat0 = await bootSESWorker('ses-boot2', async m => m); t.teardown(() => vat0.close()); await vat0.evaluate('globalThis.x = harden({a: 1})'); - const { hash } = await store.save(vat0.snapshot); + await store.saveSnapshot('vat0', 1, vat0.snapshot); - const worker = await store.load(hash, async snapshot => { + const worker = await store.loadSnapshot('vat0', async snapshot => { const xs = xsnap({ name: 'ses-resume', snapshot, os: osType(), spawn }); await xs.isReady(); return xs; @@ -145,7 +153,7 @@ test('XS + SES snapshots are long-term deterministic', async t => { filePath: _path1, compressedByteCount: _csize1, ...info1 - } = await store.save(vat.snapshot); + } = await store.saveSnapshot('vat0', 1, vat.snapshot); t.snapshot(info1, 'initial snapshot'); const bootScript = await ld.asset( @@ -157,7 +165,7 @@ test('XS + SES snapshots are long-term deterministic', async t => { filePath: _path2, compressedByteCount: _csize2, ...info2 - } = await store.save(vat.snapshot); + } = await store.saveSnapshot('vat0', 2, vat.snapshot); t.snapshot( info2, 'after SES boot - sensitive to SES-shim, XS, and supervisor', @@ -168,7 +176,7 @@ test('XS + SES snapshots are long-term deterministic', async t => { filePath: _path3, compressedByteCount: _csize3, ...info3 - } = await store.save(vat.snapshot); + } = await store.saveSnapshot('vat0', 3, vat.snapshot); t.snapshot( info3, 'after use of harden() - sensitive to SES-shim, XS, and supervisor', @@ -191,7 +199,7 @@ async function makeTestSnapshot() { ); await vat.evaluate(bootScript); await vat.evaluate('globalThis.x = harden({a: 1})'); - const info = await store.save(vat.snapshot); + const info = await store.saveSnapshot('vat0', 1, vat.snapshot); await vat.close(); return info; } diff --git a/packages/SwingSet/test/vat-warehouse/test-reload-snapshot.js b/packages/SwingSet/test/vat-warehouse/test-reload-snapshot.js index 091b7a03192..e822b2593c4 100644 --- a/packages/SwingSet/test/vat-warehouse/test-reload-snapshot.js +++ b/packages/SwingSet/test/vat-warehouse/test-reload-snapshot.js @@ -35,11 +35,9 @@ test('vat reload from snapshot', async t => { const vatID = c1.vatNameToID('target'); function getPositions() { - const lastSnapshot = kernelStorage.kvStore.get( - `local.${vatID}.lastSnapshot`, - ); + const snapshotInfo = snapStore.getSnapshotInfo(vatID); - const start = lastSnapshot ? JSON.parse(lastSnapshot).startPos : 0; + const start = snapshotInfo ? snapshotInfo.endPos : 0; const endPosition = kernelStorage.kvStore.get(`${vatID}.t.endPosition`); const end = Number(endPosition); return [start, end]; diff --git a/packages/SwingSet/test/xsnap-snapshots/test-xsnap-snapshots.js b/packages/SwingSet/test/xsnap-snapshots/test-xsnap-snapshots.js deleted file mode 100644 index 6ac319197ef..00000000000 --- a/packages/SwingSet/test/xsnap-snapshots/test-xsnap-snapshots.js +++ /dev/null @@ -1,132 +0,0 @@ -// eslint-disable-next-line import/order -import { test } from '../../tools/prepare-test-env-ava.js'; -import tmp from 'tmp'; - -// eslint-disable-next-line import/order -import { assert } from '@agoric/assert'; -import { initSwingStore } from '@agoric/swing-store'; -import { - buildKernelBundles, - initializeSwingset, - makeSwingsetController, -} from '../../src/index.js'; -import { bundleOpts } from '../util.js'; - -const bfile = name => new URL(name, import.meta.url).pathname; -test.before(async t => { - const kernelBundles = await buildKernelBundles(); - t.context.data = { kernelBundles }; -}); - -test.skip('snapshots', async t => { - const swingStorePath = tmp.dirSync({ unsafeCleanup: true }).name; - const { kernelStorage, hostStorage } = initSwingStore(swingStorePath); - const { commit } = hostStorage; - const { snapStore, kvStore } = kernelStorage; - const config = { - defaultManagerType: 'xs-worker', - snapshotInitial: 1, - snapshotInterval: 1, - bootstrap: 'bootstrap', - defaultReapInterval: 'never', // disable BOYD, only startVat+deliver - vats: { - bootstrap: { sourceSpec: bfile('bootstrap-snapshots.js') }, - }, - }; - - const { initOpts, runtimeOpts } = bundleOpts(t.context.data); - initOpts.addComms = false; - initOpts.addVattp = false; - initOpts.addTimer = false; - await initializeSwingset(config, [], kernelStorage, initOpts); - const c = await makeSwingsetController(kernelStorage, {}, runtimeOpts); - 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); - t.is(status, 'fulfilled'); - }; - - const vatID = kvStore.get('vat.name.bootstrap'); - - function getLatestSnapshot() { - const s = kvStore.get(`local.${vatID}.lastSnapshot`); - return JSON.parse(s)?.snapshotID; - } - - // return 'undefined' if the snapshotID is unrecognized, or a - // (possibly-empty) array of vatIDs that use it - function getSnapshotUsers(snapshotID) { - const v = kvStore.get(`local.snapshot.${snapshotID}`); - return v ? JSON.parse(v) : v; - } - - // the delivery of startVat and bootstrap() results in snapshot A - const sidA = getLatestSnapshot(); - t.true(snapStore.has(sidA)); - t.deepEqual(getSnapshotUsers(sidA), [vatID]); - - // increment() results in snapshot B - await run('increment'); - - const sidB = getLatestSnapshot(); - t.not(sidA, sidB); - // the DB remembers 'A' as unused, so commit() will delete it, but - // until then we should have both - t.true(snapStore.has(sidA)); - t.true(snapStore.has(sidB)); - t.deepEqual(getSnapshotUsers(sidA), []); - t.deepEqual(getSnapshotUsers(sidB), [vatID]); - - // commit() will delete the file. Ideally it would also remove the - // entire used-by entry, but they are in different DBs with - // different atomicity domains, so we currently leave the empty - // used-by entries around forever - await commit(); - - t.false(snapStore.has(sidA)); - t.true(snapStore.has(sidB)); - // t.deepEqual(getSnapshotUsers(sidA), undefined); - t.deepEqual(getSnapshotUsers(sidA), []); // not deleted - t.deepEqual(getSnapshotUsers(sidB), [vatID]); - - // this delivery does not change the vat state, so its snapshot will - // be identical to the previous one (B). In bug 5901, vatKeeper - // erroneously marked this ID as deleted. - await run('read'); - - t.is(getLatestSnapshot(), sidB); - t.true(snapStore.has(sidB)); - t.deepEqual(getSnapshotUsers(sidA), []); // not deleted - t.deepEqual(getSnapshotUsers(sidB), [vatID]); - - // in the buggy version, this commit() deleted B - await commit(); - t.is(getLatestSnapshot(), sidB); - t.true(snapStore.has(sidB)); // .. so this failed - t.deepEqual(getSnapshotUsers(sidA), []); // not deleted - t.deepEqual(getSnapshotUsers(sidB), [vatID]); - - await run('increment'); // results in snapshot C - const sidC = getLatestSnapshot(); - t.not(sidC, sidB); - t.true(snapStore.has(sidB)); - t.true(snapStore.has(sidC)); - t.deepEqual(getSnapshotUsers(sidA), []); // not deleted - t.deepEqual(getSnapshotUsers(sidB), []); - t.deepEqual(getSnapshotUsers(sidC), [vatID]); - - // the commit() will delete B now that it is unused - await commit(); - // in the buggy version, commit() failed because B was already deleted - t.false(snapStore.has(sidB)); - t.true(snapStore.has(sidC)); - t.deepEqual(getSnapshotUsers(sidA), []); // not deleted - t.deepEqual(getSnapshotUsers(sidB), []); // not deleted - t.deepEqual(getSnapshotUsers(sidC), [vatID]); -}); diff --git a/packages/swing-store/src/snapStore.js b/packages/swing-store/src/snapStore.js index 0f3818d3900..3fa53563dcd 100644 --- a/packages/swing-store/src/snapStore.js +++ b/packages/swing-store/src/snapStore.js @@ -12,7 +12,7 @@ import { } from '@agoric/internal'; /** - * @typedef {object} SnapshotInfo + * @typedef {object} SnapshotResult * @property {string} hash sha256 hash of (uncompressed) snapshot * @property {number} rawByteCount size of (uncompressed) snapshot * @property {number} rawSaveSeconds time to save (uncompressed) snapshot @@ -20,12 +20,23 @@ import { * @property {number} compressSeconds time to compress and save snapshot */ +/** + * @typedef {object} SnapshotInfo + * @property {number} endPos + * @property {string} hash + * @property {number} uncompressedSize + * @property {number} compressedSize + */ + /** * @typedef {{ - * has: (hash: string) => boolean, - * load: (hash: string, loadRaw: (filePath: string) => Promise) => Promise, - * save: (saveRaw: (filePath: string) => Promise) => Promise, - * deleteSnapshot: (hash: string) => void, + * hasHash: (vatID: string, hash: string) => boolean, + * loadSnapshot: (vatID: string, loadRaw: (filePath: string) => Promise) => Promise, + * saveSnapshot: (vatID: string, endPos: number, saveRaw: (filePath: string) => Promise) => Promise, + * deleteVatSnapshots: (vatID: string) => void, + * deleteAllUnusedSnapshots: () => void, + * deleteSnapshotByHash: (vatID: string, hash: string) => void, + * getSnapshotInfo: (vatID: string) => SnapshotInfo, * }} SnapStore */ @@ -51,10 +62,13 @@ export const buffer = async inStream => { /** @type {SnapStore} */ export const ephemeralSnapStore = { - has: fail, - load: fail, - save: fail, - deleteSnapshot: fail, + hasHash: fail, + loadSnapshot: fail, + saveSnapshot: fail, + deleteVatSnapshots: fail, + deleteAllUnusedSnapshots: fail, + deleteSnapshotByHash: fail, + getSnapshotInfo: fail, }; const finished = promisify(finishedCallback); @@ -96,16 +110,16 @@ export function makeSnapStore( ) { db.exec(` CREATE TABLE IF NOT EXISTS snapshots ( + vatID TEXT, + endPos INTEGER, + inUse INTEGER, hash TEXT, + uncompressedSize INTEGER, + compressedSize INTEGER, compressedSnapshot BLOB, - PRIMARY KEY (hash) + PRIMARY KEY (vatID, endPos) ) `); - const sqlSaveSnapshot = db.prepare(` - INSERT INTO snapshots (hash, compressedSnapshot) - VALUES (?, ?) - ON CONFLICT DO NOTHING - `); /** @type {(opts: unknown) => Promise} */ const ptmpName = promisify(tmpName); @@ -123,15 +137,38 @@ export function makeSnapStore( }); }; + const sqlDeleteAllUnusedSnapshots = db.prepare(` + DELETE FROM snapshots + WHERE inUse = 0 + `); + + function deleteAllUnusedSnapshots() { + sqlDeleteAllUnusedSnapshots.run(); + } + + const sqlStopUsingLastSnapshot = db.prepare(` + UPDATE snapshots + SET inUse = 0 + WHERE inUse = 1 AND vatID = ? + `); + + const sqlSaveSnapshot = db.prepare(` + INSERT OR REPLACE INTO snapshots + (vatID, endPos, inUse, hash, uncompressedSize, compressedSize, compressedSnapshot) + VALUES (?, ?, 1, ?, ?, ?, ?) + `); + /** * Generates a new XS heap snapshot, stores a gzipped copy of it into the * snapshots table, and reports information about the process, including * snapshot size and timing metrics. * + * @param {string} vatID + * @param {number} endPos * @param {(filePath: string) => Promise} saveRaw - * @returns {Promise} + * @returns {Promise} */ - async function save(saveRaw) { + async function saveSnapshot(vatID, endPos, saveRaw) { const cleanup = []; return aggregateTryFinally( async () => { @@ -165,8 +202,19 @@ export function makeSnapStore( await finished(snapReader); const h = hashStream.digest('hex'); - sqlSaveSnapshot.run(h, compressedSnapshot); + sqlStopUsingLastSnapshot.run(vatID); + if (!keepSnapshots) { + deleteAllUnusedSnapshots(); + } compressedByteCount = compressedSnapshot.length; + sqlSaveSnapshot.run( + vatID, + endPos, + h, + rawByteCount, + compressedByteCount, + compressedSnapshot, + ); return h; }); @@ -190,35 +238,41 @@ export function makeSnapStore( const sqlHasHash = db.prepare(` SELECT COUNT(*) FROM snapshots - WHERE hash = ? + WHERE vatID = ? AND hash = ? `); sqlHasHash.pluck(true); /** + * @param {string} vatID * @param {string} hash * @returns {boolean} */ - function has(hash) { - return !!sqlHasHash.get(hash); + function hasHash(vatID, hash) { + return !!sqlHasHash.get(vatID, hash); } const sqlLoadSnapshot = db.prepare(` - SELECT compressedSnapshot + SELECT hash, compressedSnapshot FROM snapshots - WHERE hash = ? + WHERE vatID = ? + ORDER BY endPos DESC + LIMIT 1 `); - sqlLoadSnapshot.pluck(true); /** - * @param {string} hash + * Loads the most recent snapshot for a given vat. + * + * @param {string} vatID * @param {(filePath: string) => Promise} loadRaw * @template T */ - async function load(hash, loadRaw) { + async function loadSnapshot(vatID, loadRaw) { const cleanup = []; return aggregateTryFinally( async () => { - const compressedSnapshot = sqlLoadSnapshot.get(hash); + const loadInfo = sqlLoadSnapshot.get(vatID); + assert(loadInfo, `no snapshot available for vat ${vatID}`); + const { hash, compressedSnapshot } = loadInfo; const gzReader = Readable.from(compressedSnapshot); cleanup.push(() => gzReader.destroy()); const snapReader = gzReader.pipe(createGunzip()); @@ -246,8 +300,7 @@ export function makeSnapStore( const snapWriterClose = cleanup.pop(); snapWriterClose(); - const result = await loadRaw(path); - return result; + return loadRaw(path); }, async () => { await PromiseAllOrErrors( @@ -257,19 +310,51 @@ export function makeSnapStore( ); } - const sqlDeleteSnapshot = db.prepare(` + const sqlGetSnapshotInfo = db.prepare(` + SELECT endPos, hash, uncompressedSize, compressedSize + FROM snapshots + WHERE vatID = ? + ORDER BY endPos DESC + LIMIT 1 + `); + + function getSnapshotInfo(vatID) { + return /** @type {SnapshotInfo} */ (sqlGetSnapshotInfo.get(vatID)); + } + + const sqlDeleteVatSnapshots = db.prepare(` DELETE FROM snapshots - WHERE hash = ? + WHERE vatID = ? `); /** + * @param {string} vatID + */ + function deleteVatSnapshots(vatID) { + sqlDeleteVatSnapshots.run(vatID); + } + + const sqlDeleteSnapshotByHash = db.prepare(` + DELETE FROM snapshots + WHERE vatID = ? AND hash = ? + `); + + /** + * @param {string} vatID * @param {string} hash */ - function deleteSnapshot(hash) { - if (!keepSnapshots) { - sqlDeleteSnapshot.run(hash); - } + function deleteSnapshotByHash(vatID, hash) { + sqlDeleteSnapshotByHash.run(vatID, hash); } - return freeze({ has, load, save, deleteSnapshot }); + return freeze({ + loadSnapshot, + saveSnapshot, + deleteVatSnapshots, + getSnapshotInfo, + + hasHash, + deleteAllUnusedSnapshots, + deleteSnapshotByHash, + }); } diff --git a/packages/swing-store/src/swingStore.js b/packages/swing-store/src/swingStore.js index 07a2443ffaf..4664f93a9e5 100644 --- a/packages/swing-store/src/swingStore.js +++ b/packages/swing-store/src/swingStore.js @@ -39,7 +39,7 @@ export function makeSnapStoreIO() { * * @typedef { import('./snapStore').SnapStore } SnapStore * - * @typedef { import('./snapStore').SnapshotInfo } SnapshotInfo + * @typedef { import('./snapStore').SnapshotResult } SnapshotResult * * @typedef { number } StreamPosition * diff --git a/packages/swing-store/test/test-snapstore.js b/packages/swing-store/test/test-snapstore.js index 092dae5990f..eb3a7023da1 100644 --- a/packages/swing-store/test/test-snapstore.js +++ b/packages/swing-store/test/test-snapstore.js @@ -24,7 +24,7 @@ test('build temp file; compress to cache file', async t => { measureSeconds: makeMeasureSeconds(() => 0), }); let keepTmp = ''; - const result = await store.save(async filePath => { + const result = await store.saveSnapshot('fakeVatID', 47, async filePath => { t.falsy(fs.existsSync(filePath)); fs.writeFileSync(filePath, 'abc'); keepTmp = filePath; @@ -38,10 +38,18 @@ test('build temp file; compress to cache file', async t => { rawSaveSeconds: 0, compressSeconds: 0, }); - t.is(store.has(hash), true); + const snapshotInfo = store.getSnapshotInfo('fakeVatID'); + t.deepEqual(snapshotInfo, { + endPos: 47, + hash: expectedHash, + uncompressedSize: 3, + compressedSize: 23, + }); + t.is(store.hasHash('fakeVatID', hash), true); const zero = '0000000000000000000000000000000000000000000000000000000000000000'; - t.is(store.has(zero), false); + t.is(store.hasHash('fakeVatID', zero), false); + t.is(store.hasHash('nonexistentVatID', hash), false); t.falsy( fs.existsSync(keepTmp), 'temp file should have been deleted after withTempName', @@ -69,12 +77,12 @@ test('snapStore prepare / commit delete is robust', async t => { measureSeconds: makeMeasureSeconds(() => 0), }; const db = sqlite3(':memory:'); - const store = makeSnapStore(db, io); + const store = makeSnapStore(db, io, { keepSnapshots: true }); const hashes = []; for (let i = 0; i < 5; i += 1) { // eslint-disable-next-line no-await-in-loop - const { hash } = await store.save(async fn => + const { hash } = await store.saveSnapshot('fakeVatID2', i, async fn => fs.promises.writeFile(fn, `file ${i}`), ); hashes.push(hash); @@ -87,14 +95,29 @@ test('snapStore prepare / commit delete is robust', async t => { t.is(sqlCountSnapshots.get(), 5); - store.deleteSnapshot(hashes[2]); + store.deleteSnapshotByHash('fakeVatID2', hashes[2]); t.is(sqlCountSnapshots.get(), 4); // Restore (re-save) between prepare and commit. - store.deleteSnapshot(hashes[3]); - await store.save(async fn => fs.promises.writeFile(fn, `file 3`)); - t.true(store.has(hashes[3])); + store.deleteSnapshotByHash('fakeVatID2', hashes[3]); + await store.saveSnapshot('fakeVatID3', 29, async fn => + fs.promises.writeFile(fn, `file 3`), + ); + t.true(store.hasHash('fakeVatID3', hashes[3])); - hashes.forEach(store.deleteSnapshot); + store.deleteVatSnapshots('fakeVatID2'); + t.is(sqlCountSnapshots.get(), 1); + store.deleteVatSnapshots('fakeVatID3'); t.is(sqlCountSnapshots.get(), 0); + + for (let i = 0; i < 5; i += 1) { + // eslint-disable-next-line no-await-in-loop + const { hash } = await store.saveSnapshot('fakeVatID4', i, async fn => + fs.promises.writeFile(fn, `file ${i}`), + ); + hashes.push(hash); + } + t.is(sqlCountSnapshots.get(), 5); + store.deleteAllUnusedSnapshots(); + t.is(sqlCountSnapshots.get(), 1); });