diff --git a/packages/SwingSet/src/capdata.js b/packages/SwingSet/src/capdata.js index cb203ea276f..bd6fa3cc5d9 100644 --- a/packages/SwingSet/src/capdata.js +++ b/packages/SwingSet/src/capdata.js @@ -9,7 +9,7 @@ import { assert, details as X } from '@agoric/assert'; * @param {any} capdata The object to be tested * @throws {Error} if, upon inspection, the parameter does not satisfy the above * criteria. - * @returns {asserts capdata is CapData} + * @returns {asserts capdata is CapData} */ export function insistCapData(capdata) { assert.typeof( diff --git a/packages/SwingSet/src/controller.js b/packages/SwingSet/src/controller.js index 0cb033f63d6..71cdcbf61c0 100644 --- a/packages/SwingSet/src/controller.js +++ b/packages/SwingSet/src/controller.js @@ -124,6 +124,7 @@ export function makeStartXSnap(bundles, { snapstorePath, env, spawn }) { * slogCallbacks?: unknown, * slogFile?: string, * testTrackDecref?: unknown, + * warehousePolicy?: { maxVatsOnline?: number }, * snapstorePath?: string, * spawn?: typeof import('child_process').spawn, * env?: Record @@ -148,6 +149,7 @@ export async function makeSwingsetController( slogFile, snapstorePath, spawn = ambientSpawn, + warehousePolicy = {}, } = runtimeOptions; if (typeof Compartment === 'undefined') { throw Error('SES must be installed before calling makeSwingsetController'); @@ -303,7 +305,8 @@ export async function makeSwingsetController( gcAndFinalize: makeGcAndFinalize(engineGC), }; - const kernelOptions = { verbose }; + const kernelOptions = { verbose, warehousePolicy }; + /** @type { ReturnType } */ const kernel = buildKernel(kernelEndowments, deviceEndowments, kernelOptions); if (runtimeOptions.verbose) { @@ -312,6 +315,13 @@ export async function makeSwingsetController( await kernel.start(); + /** + * @param {T} x + * @returns {T} + * @template T + */ + const defensiveCopy = x => JSON.parse(JSON.stringify(x)); + // the kernel won't leak our objects into the Vats, we must do // the same in this wrapper const controller = harden({ @@ -322,7 +332,7 @@ export async function makeSwingsetController( writeSlogObject, dump() { - return JSON.parse(JSON.stringify(kernel.dump())); + return defensiveCopy(kernel.dump()); }, verboseDebugMode(flag) { @@ -342,7 +352,11 @@ export async function makeSwingsetController( }, getStats() { - return JSON.parse(JSON.stringify(kernel.getStats())); + return defensiveCopy(kernel.getStats()); + }, + + getStatus() { + return defensiveCopy(kernel.getStatus()); }, // these are for tests @@ -393,6 +407,7 @@ export async function makeSwingsetController( * slogCallbacks?: unknown, * testTrackDecref?: unknown, * snapstorePath?: string, + * warehousePolicy?: { maxVatsOnline?: number }, * }} runtimeOptions * @typedef { import('@agoric/swing-store-simple').KVStore } KVStore */ @@ -408,12 +423,14 @@ export async function buildVatController( debugPrefix, slogCallbacks, snapstorePath, + warehousePolicy, } = runtimeOptions; const actualRuntimeOptions = { verbose, debugPrefix, slogCallbacks, snapstorePath, + warehousePolicy, }; const initializationOptions = { verbose, kernelBundles }; let bootstrapResult; diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 2af6295b65e..37844e86baa 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -3,6 +3,7 @@ import { assert, details as X } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; import { assertKnownOptions } from '../assertOptions'; import { makeVatManagerFactory } from './vatManager/factory'; +import { makeVatWarehouse } from './vatManager/vat-warehouse'; import makeDeviceManager from './deviceManager'; import { wrapStorage } from './state/storageWrapper'; import makeKernelKeeper from './state/kernelKeeper'; @@ -20,6 +21,7 @@ import { getKpidsToRetire } from './cleanup'; import { makeVatRootObjectSlot, makeVatLoader } from './loadVat'; import { makeDeviceTranslators } from './deviceTranslator'; +import { notifyTermination } from './notifyTermination'; function abbreviateReplacer(_, arg) { if (typeof arg === 'bigint') { @@ -111,7 +113,11 @@ export default function buildKernel( gcAndFinalize, } = kernelEndowments; deviceEndowments = { ...deviceEndowments }; // copy so we can modify - const { verbose, defaultManagerType = 'local' } = kernelOptions; + const { + verbose, + defaultManagerType = 'local', + warehousePolicy, + } = kernelOptions; const logStartup = verbose ? console.debug : () => 0; const { kvStore, streamStore } = hostStorage; @@ -134,20 +140,12 @@ export default function buildKernel( let started = false; /** - * @typedef {{ - * manager: VatManager, - * enablePipelining: boolean, - * notifyTermination: (s: boolean, info: unknown) => void, - * translators: ReturnType, - * }} VatInfo * @typedef {{ * manager: unknown, * translators: ReturnType, * }} DeviceInfo */ const ephemeral = { - /** @type {Map }> } key is vatID */ - vats: new Map(), /** @type { Map } key is deviceID */ devices: new Map(), /** @type {string[]} */ @@ -252,7 +250,8 @@ export default function buildKernel( // error at the first opportunity let kernelPanic = null; - function panic(problem, err) { + /** @type {(problem: unknown, err?: Error) => void } */ + function panic(problem, err = undefined) { console.error(`##### KERNEL PANIC: ${problem} #####`); kernelPanic = err || new Error(`kernel panic ${problem}`); } @@ -335,26 +334,37 @@ export default function buildKernel( doResolve(expectedDecider, [[kpid, true, errorData]]); } - function removeVatManager(vatID, shouldReject, info) { - insistCapData(info); - const old = ephemeral.vats.get(vatID); - assert(old, `no such vat: ${vatID}`); - ephemeral.vats.delete(vatID); - old.notifyTermination(shouldReject, info); - return old.manager.shutdown(); - } - + /** + * Terminate a vat; that is: delete vat DB state, + * resolve orphaned promises, notify parent, and + * shutdown worker. + * + * @param {string} vatID + * @param {boolean} shouldReject + * @param {SwingSetCapData} info + */ function terminateVat(vatID, shouldReject, info) { insistCapData(info); if (kernelKeeper.getVatKeeper(vatID)) { + const isDynamic = kernelKeeper.getDynamicVats().includes(vatID); const promisesToReject = kernelKeeper.cleanupAfterTerminatedVat(vatID); for (const kpid of promisesToReject) { resolveToError(kpid, VAT_TERMINATION_ERROR, vatID); } - removeVatManager(vatID, shouldReject, info).then( - () => kdebug(`terminated vat ${vatID}`), - e => console.error(`problem terminating vat ${vatID}`, e), - ); + if (isDynamic) { + notifyTermination( + vatID, + vatAdminRootKref, + shouldReject, + info, + queueToKref, + ); + } + // else... static... maybe panic??? + + // ISSUE: terminate stuff in its own crank like creation? + // eslint-disable-next-line no-use-before-define + vatWarehouse.vatWasTerminated(vatID); } } @@ -370,8 +380,8 @@ export default function buildKernel( } async function deliverAndLogToVat(vatID, kernelDelivery, vatDelivery) { - const vat = ephemeral.vats.get(vatID); - assert(vat); + // eslint-disable-next-line no-use-before-define + assert(vatWarehouse.lookup(vatID)); const vatKeeper = kernelKeeper.getVatKeeper(vatID); const crankNum = kernelKeeper.getCrankNumber(); const deliveryNum = vatKeeper.nextDeliveryNum(); // increments @@ -384,8 +394,14 @@ export default function buildKernel( kernelDelivery, vatDelivery, ); + // Ensure that the vatSlogger is available before clist translation. + kernelSlog.provideVatSlogger(vatID); try { - const deliveryResult = await vat.manager.deliver(vatDelivery); + // eslint-disable-next-line no-use-before-define + const deliveryResult = await vatWarehouse.deliverToVat( + vatID, + vatDelivery, + ); insistVatDeliveryResult(deliveryResult); finish(deliveryResult); const [status, problem] = deliveryResult; @@ -403,14 +419,15 @@ export default function buildKernel( async function deliverToVat(vatID, target, msg) { insistMessage(msg); - const vat = ephemeral.vats.get(vatID); kernelKeeper.incStat('dispatches'); kernelKeeper.incStat('dispatchDeliver'); - if (!vat) { + // eslint-disable-next-line no-use-before-define + if (!vatWarehouse.lookup(vatID)) { resolveToError(msg.result, VAT_TERMINATION_ERROR); } else { const kd = harden(['message', target, msg]); - const vd = vat.translators.kernelDeliveryToVatDelivery(kd); + // eslint-disable-next-line no-use-before-define + const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd); await deliverAndLogToVat(vatID, kd, vd); } } @@ -458,20 +475,21 @@ export default function buildKernel( const s = `data is not callable, has no method ${msg.method}`; // TODO: maybe replicate whatever happens with {}.foo() or 3.foo() // etc: "TypeError: {}.foo is not a function" - await resolveToError(msg.result, makeError(s)); + resolveToError(msg.result, makeError(s)); } // else { todo: maybe log error? } } else if (kp.state === 'rejected') { // TODO would it be simpler to redirect msg.kpid to kp? if (msg.result) { - await resolveToError(msg.result, kp.data); + resolveToError(msg.result, kp.data); } } else if (kp.state === 'unresolved') { if (!kp.decider) { kernelKeeper.addMessageToPromiseQueue(target, msg); } else { insistVatID(kp.decider); - const deciderVat = ephemeral.vats.get(kp.decider); + // eslint-disable-next-line no-use-before-define + const deciderVat = vatWarehouse.lookup(kp.decider); if (deciderVat) { if (deciderVat.enablePipelining) { await deliverToVat(kp.decider, target, msg); @@ -494,9 +512,9 @@ export default function buildKernel( const { vatID, kpid } = message; insistVatID(vatID); insistKernelType('promise', kpid); - const vat = ephemeral.vats.get(vatID); kernelKeeper.incStat('dispatches'); - if (!vat) { + // eslint-disable-next-line no-use-before-define + if (!vatWarehouse.lookup(vatID)) { kdebug(`dropping notify of ${kpid} to ${vatID} because vat is dead`); } else { const p = kernelKeeper.getKernelPromise(kpid); @@ -520,7 +538,8 @@ export default function buildKernel( resolutions.push([toResolve, kernelKeeper.getKernelPromise(toResolve)]); } const kd = harden(['notify', resolutions]); - const vd = vat.translators.kernelDeliveryToVatDelivery(kd); + // eslint-disable-next-line no-use-before-define + const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd); vatKeeper.deleteCListEntriesForKernelSlots(targets); await deliverAndLogToVat(vatID, kd, vd); } @@ -564,7 +583,8 @@ export default function buildKernel( } // eslint-disable-next-line no-use-before-define - return createVatDynamically(vatID, source, options) + return vatWarehouse + .createDynamicVat(vatID) .then(makeSuccessResponse, makeErrorResponse) .then(sendResponse) .catch(err => console.error(`error in vat creation`, err)); @@ -669,7 +689,8 @@ export default function buildKernel( // the VatManager+VatWorker will see the error case, but liveslots will // not function vatSyscallHandler(vatSyscallObject) { - if (!ephemeral.vats.get(vatID)) { + // eslint-disable-next-line no-use-before-define + if (!vatWarehouse.lookup(vatID)) { // This is a safety check -- this case should never happen unless the // vatManager is somehow confused. console.error(`vatSyscallHandler invoked on dead vat ${vatID}`); @@ -722,53 +743,22 @@ export default function buildKernel( return vatSyscallHandler; } - /* - * Take an existing VatManager (which is already configured to talk to a - * VatWorker, loaded with some vat code) and connect it to the rest of the - * kernel. The vat must be ready to go: any initial buildRootObject - * construction should have happened by this point. However the kernel - * might tell the manager to replay the transcript later, if it notices - * we're reloading a saved state vector. - */ - function addVatManager(vatID, manager, translators, managerOptions) { - // addVatManager takes a manager, not a promise for one - assert( - manager.deliver, - `manager lacks .deliver, isPromise=${manager instanceof Promise}`, - ); - const { - enablePipelining = false, - notifyTermination = () => {}, - } = managerOptions; - - ephemeral.vats.set( - vatID, - harden({ - translators, - manager, - notifyTermination, - enablePipelining: Boolean(enablePipelining), - }), - ); - } - - const { - createVatDynamically, - recreateDynamicVat, - recreateStaticVat, - loadTestVat, - } = makeVatLoader({ + const vatLoader = makeVatLoader({ vatManagerFactory, kernelSlog, makeVatConsole, - addVatManager, - queueToKref, kernelKeeper, panic, buildVatSyscallHandler, vatAdminRootKref, }); + const vatWarehouse = makeVatWarehouse( + kernelKeeper, + vatLoader, + warehousePolicy, + ); + /** * Create a dynamically generated vat for testing purposes. Such vats are * defined by providing a setup function rather than a bundle and can be @@ -808,7 +798,7 @@ export default function buildKernel( logStartup(`assigned VatID ${vatID} for test vat ${name}`); kernelKeeper.allocateVatKeeper(vatID); - await loadTestVat(vatID, setup, creationOptions); + await vatWarehouse.loadTestVat(vatID, setup, creationOptions); return vatID; } @@ -871,25 +861,7 @@ export default function buildKernel( started = true; assert(kernelKeeper.getInitialized(), X`kernel not initialized`); - // instantiate all static vats - for (const [name, vatID] of kernelKeeper.getStaticVats()) { - logStartup(`starting static vat ${name} as vat ${vatID}`); - const vatKeeper = kernelKeeper.allocateVatKeeper(vatID); - const { source, options } = vatKeeper.getSourceAndOptions(); - // eslint-disable-next-line no-await-in-loop - await recreateStaticVat(vatID, source, options); - // now the vatManager is attached and ready for transcript replay - } - - // instantiate all dynamic vats - for (const vatID of kernelKeeper.getDynamicVats()) { - logStartup(`starting dynamic vat ${vatID}`); - const vatKeeper = kernelKeeper.allocateVatKeeper(vatID); - const { source, options } = vatKeeper.getSourceAndOptions(); - // eslint-disable-next-line no-await-in-loop - await recreateDynamicVat(vatID, source, options); - // now the vatManager is attached and ready for transcript replay - } + await vatWarehouse.start(logStartup); // the admin device is endowed directly by the kernel deviceEndowments.vatAdmin = { @@ -938,30 +910,6 @@ export default function buildKernel( } } - // replay any transcripts - // This happens every time, now that initialisation is separated from - // execution. - kdebug('Replaying SwingSet transcripts'); - const oldLength = kernelKeeper.getRunQueueLength(); - for (const vatID of ephemeral.vats.keys()) { - logStartup(`Replaying transcript of vatID ${vatID}`); - const vat = ephemeral.vats.get(vatID); - if (!vat) { - logStartup(`skipping reload of dead vat ${vatID}`); - } else { - /** @type { FinishFunction } */ - const slogDone = kernelSlog.replayVatTranscript(vatID); - // eslint-disable-next-line no-await-in-loop - await vat.manager.replayTranscript(); - slogDone(); - logStartup(`finished replaying vatID ${vatID} transcript `); - const newLength = kernelKeeper.getRunQueueLength(); - if (newLength !== oldLength) { - console.log(`SPURIOUS RUNQUEUE`, kernelKeeper.dump().runQueue); - assert.fail(X`replay ${vatID} added spurious run-queue entries`); - } - } - } kernelKeeper.loadStats(); kernelKeeper.incrementCrankNumber(); } @@ -1018,8 +966,7 @@ export default function buildKernel( // mostly used by tests, only needed with thread/process-based workers function shutdown() { - const vatRecs = Array.from(ephemeral.vats.values()); - return Promise.all(vatRecs.map(rec => rec.manager.shutdown())); + return vatWarehouse.shutdown(); } function kpRegisterInterest(kpid) { @@ -1071,6 +1018,13 @@ export default function buildKernel( getStats() { return kernelKeeper.getStats(); }, + + getStatus() { + return harden({ + activeVats: vatWarehouse.activeVatsInfo(), + }); + }, + dump() { // note: dump().log is not deterministic, since log() does not go // through the syscall interface (and we replay transcripts one vat at diff --git a/packages/SwingSet/src/kernel/loadVat.js b/packages/SwingSet/src/kernel/loadVat.js index 91afaef7910..728161e7147 100644 --- a/packages/SwingSet/src/kernel/loadVat.js +++ b/packages/SwingSet/src/kernel/loadVat.js @@ -1,7 +1,6 @@ import { assert, details as X } from '@agoric/assert'; import { assertKnownOptions } from '../assertOptions'; import { makeVatSlot } from '../parseVatSlots'; -import { insistCapData } from '../capdata'; import { makeVatTranslators } from './vatTranslator'; export function makeVatRootObjectSlot() { @@ -13,8 +12,6 @@ export function makeVatLoader(stuff) { vatManagerFactory, kernelSlog, makeVatConsole, - addVatManager, - queueToKref, kernelKeeper, panic, buildVatSyscallHandler, @@ -27,14 +24,20 @@ export function makeVatLoader(stuff) { * * @param { string } vatID The pre-allocated vatID * @param {*} source The source object implementing the vat + * @param { ReturnType } translators * @param {*} dynamicOptions Options bag governing vat creation * - * @returns {Promise} The vatID of the newly created vat + * @returns {Promise} */ - function createVatDynamically(vatID, source, dynamicOptions = {}) { + function createVatDynamically( + vatID, + source, + translators, + dynamicOptions = {}, + ) { assert(vatAdminRootKref, `initializeKernel did not set vatAdminRootKref`); // eslint-disable-next-line no-use-before-define - return create(vatID, source, dynamicOptions, true); + return create(vatID, source, translators, dynamicOptions, true); } /** @@ -42,13 +45,14 @@ export function makeVatLoader(stuff) { * * @param {string} vatID The vatID of the vat to create * @param {*} source The source object implementing the vat + * @param { ReturnType } translators * @param {*} dynamicOptions Options bag governing vat creation * - * @returns {Promise} fires when the vat is ready for messages + * @returns {Promise} fires when the vat is ready for messages */ - function recreateDynamicVat(vatID, source, dynamicOptions) { + function recreateDynamicVat(vatID, source, translators, dynamicOptions) { // eslint-disable-next-line no-use-before-define - return create(vatID, source, dynamicOptions, true).catch(err => + return create(vatID, source, translators, dynamicOptions, true).catch(err => panic(`unable to re-create vat ${vatID}`, err), ); // if we fail to recreate the vat during replay, crash the kernel, @@ -60,14 +64,15 @@ export function makeVatLoader(stuff) { * * @param {string} vatID The vatID of the vat to create * @param {*} source The source object implementing the vat + * @param { ReturnType } translators * @param {*} staticOptions Options bag governing vat creation * - * @returns {Promise} A Promise which fires (with undefined) when the + * @returns {Promise} A Promise which fires when the * vat is ready for messages. */ - function recreateStaticVat(vatID, source, staticOptions) { + function recreateStaticVat(vatID, source, translators, staticOptions) { // eslint-disable-next-line no-use-before-define - return create(vatID, source, staticOptions, false).catch(err => + return create(vatID, source, translators, staticOptions, false).catch(err => panic(`unable to re-create vat ${vatID}`, err), ); } @@ -99,13 +104,17 @@ export function makeVatLoader(stuff) { * wait. * * @param {string} vatID The vatID for the new vat - * @param {*} source an object which either has a `bundle` (JSON-serializable + * @param {{bundle: Bundle} | {bundleName: string}} source + * an object which either has a `bundle` (JSON-serializable * data) or a `bundleName` string. The bundle defines the vat, and should * be generated by calling bundle-source on a module with an export named * `makeRootObject()` (or possibly `setup()` if the 'enableSetup' option is * true). If `bundleName` is used, it must identify a bundle already known * to the kernel (via the `config.bundles` table) which satisfies these * constraints. + * + * @param { ReturnType } translators + * * @param {*} options an options bag. These options are currently understood: * * 'metered' if true, subjects the new dynamic vat to a meter that limits @@ -134,10 +143,10 @@ export function makeVatLoader(stuff) { * if false, it's a static vat (these have differences in their allowed * options and some of their option defaults). * - * @returns {Promise} A Promise which fires (with undefined) when the + * @returns {Promise} A Promise which fires when the * vat is ready for messages. */ - async function create(vatID, source, options, isDynamic) { + async function create(vatID, source, translators, options, isDynamic) { assert(source.bundle || source.bundleName, 'broken source'); const vatSourceBundle = source.bundle || kernelKeeper.getBundle(source.bundleName); @@ -162,7 +171,6 @@ export function makeVatLoader(stuff) { virtualObjectCacheSize, name, } = options; - let terminated = false; // TODO: maybe hash the bundle object somehow for the description const sourceDesc = source.bundle @@ -170,25 +178,7 @@ export function makeVatLoader(stuff) { : `from bundleName: ${source.bundleName}`; const description = `${options.description || ''} (${sourceDesc})`.trim(); - function notifyTermination(shouldReject, info) { - insistCapData(info); - if (terminated) { - return; - } - terminated = true; - - // Embedding the info capdata into the arguments list, taking advantage of - // the fact that neither vatID (which is a string) nor shouldReject (which - // is a boolean) can contain any slots. - const args = { - body: JSON.stringify([vatID, shouldReject, JSON.parse(info.body)]), - slots: info.slots, - }; - - queueToKref(vatAdminRootKref, 'vatTerminated', args, 'logFailure'); - } - - kernelSlog.addVat( + const { starting } = kernelSlog.provideVatSlogger( vatID, isDynamic, description, @@ -197,6 +187,7 @@ export function makeVatLoader(stuff) { managerType, vatParameters, ); + const managerOptions = { managerType, bundle: vatSourceBundle, @@ -204,7 +195,6 @@ export function makeVatLoader(stuff) { enableDisavow, enableSetup, enablePipelining, - notifyTermination, vatConsole: makeVatConsole('vat', vatID), liveSlotsConsole: makeVatConsole('ls', vatID), vatParameters, @@ -212,17 +202,16 @@ export function makeVatLoader(stuff) { name, }; - const translators = makeVatTranslators(vatID, kernelKeeper); const vatSyscallHandler = buildVatSyscallHandler(vatID, translators); - const finish = kernelSlog.startup(vatID); + const finish = starting && kernelSlog.startup(vatID); const manager = await vatManagerFactory( vatID, managerOptions, vatSyscallHandler, ); - finish(); - addVatManager(vatID, manager, translators, managerOptions); + starting && finish(); + return manager; } async function loadTestVat(vatID, setup, creationOptions) { @@ -239,7 +228,7 @@ export function makeVatLoader(stuff) { managerOptions, vatSyscallHandler, ); - addVatManager(vatID, manager, translators, managerOptions); + return manager; } return harden({ diff --git a/packages/SwingSet/src/kernel/notifyTermination.js b/packages/SwingSet/src/kernel/notifyTermination.js new file mode 100644 index 00000000000..0726ee57428 --- /dev/null +++ b/packages/SwingSet/src/kernel/notifyTermination.js @@ -0,0 +1,30 @@ +// @ts-check + +import { insistCapData } from '../capdata'; + +/** + * @param {string} vatID + * @param {string} vatAdminRootKref + * @param {boolean} shouldReject + * @param {CapData} info + * @param {(kref: string, method: string, args: unknown, policy?: string) => void} queueToKref + */ +export function notifyTermination( + vatID, + vatAdminRootKref, + shouldReject, + info, + queueToKref, +) { + insistCapData(info); + + // Embedding the info capdata into the arguments list, taking advantage of + // the fact that neither vatID (which is a string) nor shouldReject (which + // is a boolean) can contain any slots. + const args = { + body: JSON.stringify([vatID, shouldReject, JSON.parse(info.body)]), + slots: info.slots, + }; + + queueToKref(vatAdminRootKref, 'vatTerminated', args, 'logFailure'); +} diff --git a/packages/SwingSet/src/kernel/parseKernelSlots.js b/packages/SwingSet/src/kernel/parseKernelSlots.js index 35737d8edb3..ce7eb3eb9d0 100644 --- a/packages/SwingSet/src/kernel/parseKernelSlots.js +++ b/packages/SwingSet/src/kernel/parseKernelSlots.js @@ -17,7 +17,7 @@ import { assert, details as X } from '@agoric/assert'; * id: Nat * } * - * @param {string} s The string to be parsed, as described above. + * @param {unknown} s The string to be parsed, as described above. * * @returns {{type: 'object' | 'device' | 'promise', id: number}} a kernel slot object corresponding to the parameter. * diff --git a/packages/SwingSet/src/kernel/slogger.js b/packages/SwingSet/src/kernel/slogger.js index 3a895896053..0957aec9706 100644 --- a/packages/SwingSet/src/kernel/slogger.js +++ b/packages/SwingSet/src/kernel/slogger.js @@ -82,7 +82,7 @@ export function makeDummySlogger(slogCallbacks, makeConsole) { slogCallbacks, ); const dummySlogger = harden({ - addVat: reg('addVat', () => 0), + provideVatSlogger: reg('provideVatSlogger', () => 0), vatConsole: reg('vatConsole', () => makeConsole('disabled slogger')), startup: reg('startup', () => () => 0), // returns nop finish() function replayVatTranscript: reg('replayVatTranscript', () => () => 0), @@ -189,7 +189,7 @@ export function makeSlogger(slogCallbacks, writeObj) { }); } - function addVat( + function provideVatSlogger( vatID, dynamic, description, @@ -198,7 +198,10 @@ export function makeSlogger(slogCallbacks, writeObj) { managerType, vatParameters, ) { - assert(!vatSlogs.has(vatID), X`already have slog for ${vatID}`); + const found = vatSlogs.get(vatID); + if (found) { + return { vatSlog: found, starting: false }; + } const vatSlog = makeVatSlog(vatID); vatSlogs.set(vatID, vatSlog); write({ @@ -211,7 +214,7 @@ export function makeSlogger(slogCallbacks, writeObj) { vatParameters, vatSourceBundle, }); - return vatSlog; + return { vatSlog, starting: true }; } function replayVatTranscript(vatID) { @@ -230,7 +233,7 @@ export function makeSlogger(slogCallbacks, writeObj) { slogCallbacks, ); const slogger = harden({ - addVat: reg('addVat', addVat), + provideVatSlogger: reg('provideVatSlogger', provideVatSlogger), vatConsole: reg('vatConsole', (vatID, ...args) => vatSlogs.get(vatID).vatConsole(...args), ), diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 3bb8b1dc1b8..cfe685142b8 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -646,7 +646,7 @@ export default function makeKernelKeeper(kvStore, streamStore, kernelSlog) { * Note that currently we are only reference counting promises, but ultimately * we intend to keep track of all objects with kernel slots. * - * @param {string} kernelSlot The kernel slot whose refcount is to be incremented. + * @param {unknown} kernelSlot The kernel slot whose refcount is to be incremented. * @param {string} _tag */ function incrementRefCount(kernelSlot, _tag) { diff --git a/packages/SwingSet/src/kernel/vatManager/factory.js b/packages/SwingSet/src/kernel/vatManager/factory.js index d083fc1a5bd..ade8121e5b5 100644 --- a/packages/SwingSet/src/kernel/vatManager/factory.js +++ b/packages/SwingSet/src/kernel/vatManager/factory.js @@ -60,7 +60,6 @@ export function makeVatManagerFactory({ 'enableDisavow', 'enableSetup', 'liveSlotsConsole', - 'notifyTermination', 'virtualObjectCacheSize', 'vatParameters', 'vatConsole', diff --git a/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js b/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js index d4a11100c07..bba76e0798d 100644 --- a/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js +++ b/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js @@ -20,13 +20,13 @@ const decoder = new TextDecoder(); * allVatPowers: VatPowers, * kernelKeeper: KernelKeeper, * kernelSlog: KernelSlog, - * startXSnap: (name: string, handleCommand: SyncHandler, metered?: boolean) => Promise, + * startXSnap: (name: string, handleCommand: AsyncHandler, metered?: boolean) => Promise, * testLog: (...args: unknown[]) => void, * }} tools * @returns { VatManagerFactory } * * @typedef { { moduleFormat: 'getExport', source: string } } ExportBundle - * @typedef { (msg: Uint8Array) => Uint8Array } SyncHandler + * @typedef { (msg: Uint8Array) => Promise } AsyncHandler * @typedef { ReturnType } XSnap */ export function makeXsSubprocessFactory({ @@ -96,8 +96,8 @@ export function makeXsSubprocessFactory({ } } - /** @type { (msg: Uint8Array) => Uint8Array } */ - function handleCommand(msg) { + /** @type { (msg: Uint8Array) => Promise } */ + async function handleCommand(msg) { // parentLog('handleCommand', { length: msg.byteLength }); const tagged = handleUpstream(JSON.parse(decoder.decode(msg))); return encoder.encode(JSON.stringify(tagged)); diff --git a/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js b/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js new file mode 100644 index 00000000000..c03f2fb461b --- /dev/null +++ b/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js @@ -0,0 +1,275 @@ +// @ts-check +import { assert } from '@agoric/assert'; +import { makeVatTranslators } from '../vatTranslator'; + +/** + * @param { ReturnType } kernelKeeper + * @param { ReturnType } vatLoader + * @param {{ maxVatsOnline?: number }=} policyOptions + * + * @typedef {(syscall: VatSyscallObject) => ['error', string] | ['ok', null] | ['ok', Capdata]} VatSyscallHandler + * @typedef {{ body: string, slots: unknown[] }} Capdata + * @typedef { [unknown, ...unknown[]] } Tagged + * @typedef { { moduleFormat: string }} Bundle + */ +export function makeVatWarehouse(kernelKeeper, vatLoader, policyOptions) { + const { maxVatsOnline = 50 } = policyOptions || {}; + // console.debug('makeVatWarehouse', { policyOptions }); + + /** + * @typedef {{ + * manager: VatManager, + * enablePipelining: boolean, + * options: { name?: string, description?: string }, + * }} VatInfo + * @typedef { ReturnType } VatTranslators + */ + const ephemeral = { + /** @type {Map } key is vatID */ + vats: new Map(), + }; + + /** @type {Map } */ + const xlate = new Map(); + /** @param { string } vatID */ + function provideTranslators(vatID) { + let translators = xlate.get(vatID); + if (!translators) { + // NOTE: makeVatTranslators assumes + // vatKeeper for this vatID is available. + translators = makeVatTranslators(vatID, kernelKeeper); + xlate.set(vatID, translators); + } + return translators; + } + + /** + * @param {string} vatID + * @param {boolean} recreate + * @returns { Promise } + */ + async function ensureVatOnline(vatID, recreate) { + const info = ephemeral.vats.get(vatID); + if (info) return info; + + const vatKeeper = + kernelKeeper.getVatKeeper(vatID) || kernelKeeper.allocateVatKeeper(vatID); + const { source, options } = vatKeeper.getSourceAndOptions(); + + const translators = provideTranslators(vatID); + + const chooseLoader = () => { + if (recreate) { + const isDynamic = kernelKeeper.getDynamicVats().includes(vatID); + if (isDynamic) { + return vatLoader.recreateDynamicVat; + } else { + return vatLoader.recreateStaticVat; + } + } else { + return vatLoader.createVatDynamically; + } + }; + // console.log('provide: creating from bundle', vatID); + const manager = await chooseLoader()(vatID, source, translators, options); + assert(manager, `no vat manager; kernel panic?`); + + // TODO(3218): persist this option; avoid spinning up a vat that isn't pipelined + const { enablePipelining = false } = options; + + // TODO: load from snapshot + await manager.replayTranscript(); + const result = { + manager, + translators, + enablePipelining, + options, + }; + ephemeral.vats.set(vatID, result); + // eslint-disable-next-line no-use-before-define + await applyAvailabilityPolicy(vatID); + return result; + } + + /** + * Bring new dynamic vat online and run its (bootstrap) code. + * + * @param {string} vatID + */ + async function createDynamicVat(vatID) { + return ensureVatOnline(vatID, false); + } + + /** @param { typeof console.log } logStartup */ + async function start(logStartup) { + const recreate = true; // note: PANIC on failure to recreate + + // NOTE: OPTIMIZATION OPPORTUNITY: replay vats in parallel + + // instantiate all static vats + for (const [name, vatID] of kernelKeeper.getStaticVats()) { + logStartup(`allocateVatKeeper for vat ${name} as vat ${vatID}`); + // eslint-disable-next-line no-await-in-loop + await ensureVatOnline(vatID, recreate); + } + + // instantiate all dynamic vats + for (const vatID of kernelKeeper.getDynamicVats()) { + logStartup(`allocateVatKeeper for dynamic vat ${vatID}`); + // eslint-disable-next-line no-await-in-loop + await ensureVatOnline(vatID, recreate); + } + } + + /** + * @param { string } vatID + * @returns {{ enablePipelining?: boolean } + * | void // undefined if the vat is dead or never initialized + * } + */ + function lookup(vatID) { + const liveInfo = ephemeral.vats.get(vatID); + if (liveInfo) { + const { enablePipelining } = liveInfo; + return { enablePipelining }; + } + const vatKeeper = kernelKeeper.getVatKeeper(vatID); + if (vatKeeper) { + const { + options: { enablePipelining }, + } = vatKeeper.getSourceAndOptions(); + return { enablePipelining }; + } + return undefined; + } + + /** + * + * @param {string} vatID + * @param {boolean=} makeSnapshot + * @returns { Promise } + */ + async function evict(vatID, makeSnapshot = false) { + assert(!makeSnapshot, 'not implemented'); + assert(lookup(vatID)); + const info = ephemeral.vats.get(vatID); + if (!info) return undefined; + ephemeral.vats.delete(vatID); + xlate.delete(vatID); + const vatKeeper = kernelKeeper.getVatKeeper(vatID); + if (vatKeeper) { + // not terminated + vatKeeper.closeTranscript(); + } + + // console.log('evict: shutting down', vatID); + return info.manager.shutdown(); + } + + /** @type { string[] } */ + const recent = []; + + /** + * Simple fixed-size LRU cache policy + * + * TODO: policy input: did a vat get a message? how long ago? + * "important" vat option? + * options: pay $/block to keep in RAM - advisory; not consensus + * creation arg: # of vats to keep in RAM (LRU 10~50~100) + * + * @param {string} currentVatID + */ + async function applyAvailabilityPolicy(currentVatID) { + // console.log('applyAvailabilityPolicy', currentVatID, recent); + const pos = recent.indexOf(currentVatID); + // console.debug('applyAvailabilityPolicy', { currentVatID, recent, pos }); + // already most recently used + if (pos + 1 === maxVatsOnline) return; + if (pos >= 0) recent.splice(pos, 1); + recent.push(currentVatID); + // not yet full + if (recent.length <= maxVatsOnline) return; + const [lru] = recent.splice(0, 1); + // console.debug('evicting', { lru }); + await evict(lru); + } + + /** @type {(vatID: string, d: VatDeliveryObject) => Promise } */ + async function deliverToVat(vatID, delivery) { + await applyAvailabilityPolicy(vatID); + const recreate = true; // PANIC in the failure case + + const { manager } = await ensureVatOnline(vatID, recreate); + return manager.deliver(delivery); + } + + /** + * @param {string} vatID + * @param {unknown[]} kd + * @returns { VatDeliveryObject } + */ + function kernelDeliveryToVatDelivery(vatID, kd) { + const translators = provideTranslators(vatID); + + // @ts-ignore TODO: types for kernelDeliveryToVatDelivery + return translators.kernelDeliveryToVatDelivery(kd); + } + + /** + * @param {string} vatID + * @param {unknown} setup + * @param {ManagerOptions} creationOptions + */ + async function loadTestVat(vatID, setup, creationOptions) { + const manager = await vatLoader.loadTestVat(vatID, setup, creationOptions); + + const translators = provideTranslators(vatID); + + const { enablePipelining = false } = creationOptions; + + const result = { + manager, + translators, + enablePipelining, + options: {}, + }; + ephemeral.vats.set(vatID, result); + } + + /** + * @param {string} vatID + * @returns { Promise } + */ + async function vatWasTerminated(vatID) { + try { + await evict(vatID, false); + } catch (err) { + console.debug('vat termination was already reported; ignoring:', err); + } + } + + // mostly used by tests, only needed with thread/process-based workers + function shutdown() { + const work = Array.from(ephemeral.vats.values(), ({ manager }) => + manager.shutdown(), + ); + return Promise.all(work); + } + + return harden({ + start, + createDynamicVat, + loadTestVat, + lookup, + kernelDeliveryToVatDelivery, + deliverToVat, + + // mostly for testing? + activeVatsInfo: () => + [...ephemeral.vats].map(([id, { options }]) => ({ id, options })), + + vatWasTerminated, + shutdown, + }); +} +harden(makeVatWarehouse); diff --git a/packages/SwingSet/src/types.js b/packages/SwingSet/src/types.js index a970fdf3549..b047c55b80e 100644 --- a/packages/SwingSet/src/types.js +++ b/packages/SwingSet/src/types.js @@ -16,16 +16,17 @@ * enableSetup: true, * }} HasSetup * - * TODO: metered... - * + * TODO: liveSlotsConsole... * See validateManagerOptions() in factory.js + * * @typedef { 'local' | 'nodeWorker' | 'node-subprocess' | 'xs-worker' } ManagerType * @typedef {{ + * enablePipelining?: boolean, * managerType: ManagerType, * metered?: boolean, * enableDisavow?: boolean, - * vatParameters: Record, * virtualObjectCacheSize: number, + * vatParameters: Record, * name: string, * compareSyscalls?: (originalSyscall: {}, newSyscall: {}) => Error | undefined, * } & (HasBundle | HasSetup)} ManagerOptions @@ -116,7 +117,7 @@ * vatSyscallHandler: unknown) => Promise, * } } VatManagerFactory * @typedef { { deliver: (delivery: VatDeliveryObject) => Promise, - * replayTranscript: () => void, + * replayTranscript: () => Promise, * shutdown: () => Promise, * } } VatManager * @typedef { () => Promise } WaitUntilQuiescent diff --git a/packages/SwingSet/src/vats/comms/parseLocalSlots.js b/packages/SwingSet/src/vats/comms/parseLocalSlots.js index 475a877109a..17318b3470c 100644 --- a/packages/SwingSet/src/vats/comms/parseLocalSlots.js +++ b/packages/SwingSet/src/vats/comms/parseLocalSlots.js @@ -13,7 +13,7 @@ import { assert, details as X } from '@agoric/assert'; * id: Nat * } * - * @param {string} s The string to be parsed, as described above. + * @param {unknown} s The string to be parsed, as described above. * * @returns {{type: 'object' | 'promise', id: number}} a local slot object corresponding to the parameter. * diff --git a/packages/SwingSet/test/warehouse/bootstrap.js b/packages/SwingSet/test/warehouse/bootstrap.js new file mode 100644 index 00000000000..abc1c933b6a --- /dev/null +++ b/packages/SwingSet/test/warehouse/bootstrap.js @@ -0,0 +1,11 @@ +import { Far } from '@agoric/marshal'; + +export function buildRootObject() { + let vatStrongRef; + return Far('root', { + bootstrap(vats, _devices) { + // eslint-disable-next-line no-unused-vars + vatStrongRef = vats; + }, + }); +} diff --git a/packages/SwingSet/test/warehouse/test-warehouse.js b/packages/SwingSet/test/warehouse/test-warehouse.js new file mode 100644 index 00000000000..1e16a417cdf --- /dev/null +++ b/packages/SwingSet/test/warehouse/test-warehouse.js @@ -0,0 +1,86 @@ +/* global __dirname */ +// @ts-check + +import { test } from '../../tools/prepare-test-env-ava'; + +import { loadBasedir, buildVatController } from '../../src/index'; + +async function makeController(managerType, maxVatsOnline) { + const config = await loadBasedir(__dirname); + config.vats.target.creationOptions = { managerType, enableDisavow: true }; + config.vats.target2 = config.vats.target; + config.vats.target3 = config.vats.target; + config.vats.target4 = config.vats.target; + const warehousePolicy = { maxVatsOnline }; + const c = await buildVatController(config, [], { warehousePolicy }); + return c; +} + +/** @type { (body: string, slots?: string[]) => SwingSetCapData } */ +function capdata(body, slots = []) { + return harden({ body, slots }); +} + +/** @type { (args: unknown[], slots?: string[]) => SwingSetCapData } */ +function capargs(args, slots = []) { + return capdata(JSON.stringify(args), slots); +} + +const maxVatsOnline = 2; +const steps = [ + { + // After we deliver to... + vat: 'target', + // ... we expect these vats online: + online: [ + { id: 'v2', name: 'bootstrap' }, + { id: 'v1', name: 'target' }, + ], + }, + { + vat: 'target2', + online: [ + { id: 'v1', name: 'target' }, + { id: 'v3', name: 'target2' }, + ], + }, + { + vat: 'target3', + online: [ + { id: 'v3', name: 'target2' }, + { id: 'v4', name: 'target3' }, + ], + }, + { + vat: 'target4', + online: [ + { id: 'v4', name: 'target3' }, + { id: 'v5', name: 'target4' }, + ], + }, +]; + +test('4 vats in warehouse with 2 online', async t => { + const c = await makeController('xs-worker', maxVatsOnline); + t.teardown(c.shutdown); + + await c.run(); + for (const { vat, online } of steps) { + t.log('sending to vat', vat); + c.queueToVatExport(vat, 'o+0', 'append', capargs([1])); + // eslint-disable-next-line no-await-in-loop + await c.run(); + t.log( + 'max:', + maxVatsOnline, + 'expected online:', + online.map(({ id, name }) => [id, name]), + ); + t.deepEqual( + c + .getStatus() + .activeVats.map(({ id, options: { name } }) => ({ id, name })), + online, + ); + } +}); diff --git a/packages/SwingSet/test/warehouse/vat-target.js b/packages/SwingSet/test/warehouse/vat-target.js new file mode 100644 index 00000000000..e0c2b639537 --- /dev/null +++ b/packages/SwingSet/test/warehouse/vat-target.js @@ -0,0 +1,15 @@ +import { Far } from '@agoric/marshal'; + +export function buildRootObject(_vatPowers, _vatParameters) { + const contents = []; + function append(thing) { + contents.push(thing); + return harden([...contents]); + } + + const target = Far('root', { + append, + }); + + return target; +} diff --git a/packages/SwingSet/test/workers/vat-target.js b/packages/SwingSet/test/workers/vat-target.js index 1442dcbbec9..cce087e1baa 100644 --- a/packages/SwingSet/test/workers/vat-target.js +++ b/packages/SwingSet/test/workers/vat-target.js @@ -63,9 +63,16 @@ export function buildRootObject(vatPowers, vatParameters) { // crank 5: dispatch.notify(pF, false, ['data', callbackObj]) + const contents = []; + function append(thing) { + contents.push(thing); + return harden([...contents]); + } + const target = Far('root', { zero, one, + append, }); return target;