diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index e2bc4a659f2..48aa9766f2a 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -71,7 +71,8 @@ const enableKernelGC = true; // $vatSlot is one of: o+$NN/o-$NN/p+$NN/p-$NN/d+$NN/d-$NN // v$NN.c.$vatSlot = $kernelSlot = ko$NN/kp$NN/kd$NN // v$NN.nextDeliveryNum = $NN -// v$NN.t.endPosition = $NN +// v$NN.t.startPosition = $NN // inclusive +// v$NN.t.endPosition = $NN // exclusive // v$NN.vs.$key = string // v$NN.meter = m$NN // XXX does this exist? // v$NN.reapInterval = $NN or 'never' diff --git a/packages/SwingSet/src/kernel/state/vatKeeper.js b/packages/SwingSet/src/kernel/state/vatKeeper.js index 4bb1b73a625..b7f37fd46a9 100644 --- a/packages/SwingSet/src/kernel/state/vatKeeper.js +++ b/packages/SwingSet/src/kernel/state/vatKeeper.js @@ -35,6 +35,10 @@ export function initializeVatState(kvStore, streamStore, vatID) { kvStore.set(`${vatID}.p.nextID`, `${FIRST_PROMISE_ID}`); kvStore.set(`${vatID}.d.nextID`, `${FIRST_DEVICE_ID}`); kvStore.set(`${vatID}.nextDeliveryNum`, `0`); + kvStore.set( + `${vatID}.t.startPosition`, + `${JSON.stringify(streamStore.STREAM_START)}`, + ); kvStore.set( `${vatID}.t.endPosition`, `${JSON.stringify(streamStore.STREAM_START)}`, @@ -454,11 +458,14 @@ export function makeVatKeeper( * * @yields { TranscriptEntry } a stream of transcript entries */ - function* getTranscript(startPos = streamStore.STREAM_START) { + function* getTranscript(startPos) { + if (startPos === undefined) { + startPos = JSON.parse(getRequired(`${vatID}.t.startPosition`)); + } const endPos = JSON.parse(getRequired(`${vatID}.t.endPosition`)); for (const entry of streamStore.readStream( transcriptStream, - startPos, + /** @type { StreamPosition } */ (startPos), endPos, )) { yield /** @type { TranscriptEntry } */ (JSON.parse(entry)); @@ -582,7 +589,6 @@ export function makeVatKeeper( function removeSnapshotAndTranscript() { const skey = `local.${vatID}.lastSnapshot`; - const epkey = `${vatID}.t.endPosition`; if (snapStore) { const notation = kvStore.get(skey); if (notation) { @@ -596,9 +602,9 @@ export function makeVatKeeper( } } // TODO: same rollback concern - // TODO: streamStore.deleteStream(transcriptStream); - const newStart = streamStore.STREAM_START; - kvStore.set(epkey, `${JSON.stringify(newStart)}`); + + const endPos = getRequired(`${vatID}.t.endPosition`); + kvStore.set(`${vatID}.t.startPosition`, endPos); } function vatStats() { @@ -610,9 +616,13 @@ export function makeVatKeeper( const objectCount = getCount(`${vatID}.o.nextID`, FIRST_OBJECT_ID); const promiseCount = getCount(`${vatID}.p.nextID`, FIRST_PROMISE_ID); const deviceCount = getCount(`${vatID}.d.nextID`, FIRST_DEVICE_ID); - const transcriptCount = JSON.parse( + const startCount = JSON.parse( + getRequired(`${vatID}.t.startPosition`), + ).itemCount; + const endCount = JSON.parse( getRequired(`${vatID}.t.endPosition`), ).itemCount; + const transcriptCount = endCount - startCount; // TODO: Fix the downstream JSON.stringify to allow the counts to be BigInts return harden({ diff --git a/packages/SwingSet/test/upgrade/bootstrap-upgrade-replay.js b/packages/SwingSet/test/upgrade/bootstrap-upgrade-replay.js new file mode 100644 index 00000000000..4e6182083dc --- /dev/null +++ b/packages/SwingSet/test/upgrade/bootstrap-upgrade-replay.js @@ -0,0 +1,38 @@ +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; + +export function buildRootObject() { + let vatAdmin; + let uptonRoot; + let uptonAdmin; + + return Far('root', { + async bootstrap(vats, devices) { + vatAdmin = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); + }, + + async buildV1() { + // build Upton, the upgrading vat + const bcap = await E(vatAdmin).getNamedBundleCap('upton'); + const vatParameters = { version: 'v1' }; + const options = { vatParameters }; + const res = await E(vatAdmin).createVat(bcap, options); + uptonRoot = res.root; + uptonAdmin = res.adminNode; + return E(uptonRoot).phase1(); + }, + + async upgradeV2() { + // upgrade Upton to version 2 + const bcap = await E(vatAdmin).getNamedBundleCap('upton'); + const vatParameters = { version: 'v2' }; + await E(uptonAdmin).upgrade(bcap, vatParameters); + return E(uptonRoot).phase2(); + }, + + async checkReplay() { + // ask Upton to do something after a restart + return E(uptonRoot).checkReplay(); + }, + }); +} diff --git a/packages/SwingSet/test/upgrade/test-upgrade-replay.js b/packages/SwingSet/test/upgrade/test-upgrade-replay.js new file mode 100644 index 00000000000..8f98f0f06e4 --- /dev/null +++ b/packages/SwingSet/test/upgrade/test-upgrade-replay.js @@ -0,0 +1,84 @@ +// eslint-disable-next-line import/order +import { test } from '../../tools/prepare-test-env-ava.js'; + +import { assert } from '@agoric/assert'; +import { getAllState, setAllState } from '@agoric/swing-store'; +import { provideHostStorage } from '../../src/controller/hostStorage.js'; +import { + buildKernelBundles, + initializeSwingset, + makeSwingsetController, +} from '../../src/index.js'; +import { capargs } from '../util.js'; + +function bfile(name) { + return new URL(name, import.meta.url).pathname; +} + +function copy(data) { + return JSON.parse(JSON.stringify(data)); +} + +async function run(c, name, args = []) { + assert(Array.isArray(args)); + const kpid = c.queueToVatRoot('bootstrap', name, capargs(args)); + await c.run(); + const status = c.kpStatus(kpid); + const capdata = c.kpResolution(kpid); + return [status, capdata]; +} + +test.before(async t => { + const kernelBundles = await buildKernelBundles(); + t.context.data = { kernelBundles }; +}); + +test('replay after upgrade', async t => { + const config = { + bootstrap: 'bootstrap', + vats: { + bootstrap: { sourceSpec: bfile('bootstrap-upgrade-replay.js') }, + }, + bundles: { + upton: { sourceSpec: bfile('vat-upton-replay.js') }, + }, + }; + + const hostStorage1 = provideHostStorage(); + { + await initializeSwingset(copy(config), [], hostStorage1, { + kernelBundles: t.context.data.kernelBundles, + }); + const c1 = await makeSwingsetController(hostStorage1); + c1.pinVatRoot('bootstrap'); + await c1.run(); + + // create initial version + const [v1status, v1data] = await run(c1, 'buildV1'); + t.is(v1status, 'fulfilled'); + t.deepEqual(v1data, capargs(1)); + + // now perform the upgrade + const [v2status, v2data] = await run(c1, 'upgradeV2'); + t.is(v2status, 'fulfilled'); + // upgrade restart loses RAM state, hence adding 20 yields 20 rather than 21 + t.deepEqual(v2data, capargs(20)); + } + + // copy the store just to be sure + const state1 = getAllState(hostStorage1); + const hostStorage2 = provideHostStorage(); + setAllState(hostStorage2, state1); + { + const c2 = await makeSwingsetController(hostStorage2); + c2.pinVatRoot('bootstrap'); + await c2.run(); + + // do something after replay that says we're still alive + const [rstatus, rdata] = await run(c2, 'checkReplay'); + t.is(rstatus, 'fulfilled'); + + // replay retains RAM state of post-upgrade vat, hence adding 300 yields 320 + t.deepEqual(rdata, capargs(320)); + } +}); diff --git a/packages/SwingSet/test/upgrade/vat-upton-replay.js b/packages/SwingSet/test/upgrade/vat-upton-replay.js new file mode 100644 index 00000000000..24d3e614036 --- /dev/null +++ b/packages/SwingSet/test/upgrade/vat-upton-replay.js @@ -0,0 +1,22 @@ +import { Far } from '@endo/marshal'; + +export function buildRootObject() { + let counter = 0; + + return Far('root', { + phase1() { + counter += 1; + return counter; + }, + + phase2() { + counter += 20; + return counter; + }, + + checkReplay() { + counter += 300; + return counter; + }, + }); +} diff --git a/packages/SwingSet/test/vat-admin/terminate/test-terminate.js b/packages/SwingSet/test/vat-admin/terminate/test-terminate.js index ab2db020cf8..3cf3914183e 100644 --- a/packages/SwingSet/test/vat-admin/terminate/test-terminate.js +++ b/packages/SwingSet/test/vat-admin/terminate/test-terminate.js @@ -174,7 +174,7 @@ test('dead vat state removed', async t => { const kvStore = hostStorage.kvStore; t.is(kvStore.get('vat.dynamicIDs'), '["v6"]'); t.is(kvStore.get('ko26.owner'), 'v6'); - t.is(Array.from(kvStore.getKeys('v6.', 'v6/')).length, 32); + t.is(Array.from(kvStore.getKeys('v6.', 'v6/')).length, 33); controller.queueToVatRoot('bootstrap', 'phase2', capargs([])); await controller.run();