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..32e04e782bb --- /dev/null +++ b/packages/SwingSet/src/kernel/vatManager/vat-warehouse.js @@ -0,0 +1,175 @@ +// @ts-check +import { assert, q, details as X } from '@agoric/assert'; +import { makeVatTranslators } from '../vatTranslator'; + +/** + * @param { ReturnType } kernelKeeper + * @param { SwingStore['storage'] } storage + * @param { Record } factories + * @param { (vatID: string, translators: unknown) => VatSyscallHandler } buildVatSyscallHandler + * @param {{ sizeHint?: number }=} policyOptions + * + * @typedef { ReturnType } SwingStore + * @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, + storage, + factories, + buildVatSyscallHandler, + policyOptions, +) { + const { sizeHint = 10 } = policyOptions || {}; + /** @type { Map } */ + const idToManager = new Map(); + + const { parse, stringify } = JSON; + + // v$NN.source = JSON({ bundle }) or JSON({ bundleName }) + // v$NN.options = JSON + // TODO: check against createVatDynamically + // TODO transaction boundaries? + // ensure kernel doesn't start transaction until after it gets manager? + const idToInitDetail = { + /** @type { (vatID: string) => { vatID: string, bundle: Bundle, options: ManagerOptions } } */ + get(vatID) { + const source = storage.get(`${vatID}.source`); + assert(source, X`${q(vatID)} missing .source`); + const { bundle } = parse(source); + assert(bundle, X`${q(vatID)}: bundleName not supported`); + + const optionsData = storage.get(`${vatID}.options`); + assert(optionsData, X`${q(vatID)} missing .options`); + const options = parse(optionsData); + // TODO: validate options + return { vatID, bundle, options }; + }, + /** @type { (vatID: string, d: { bundle: Bundle, options: ManagerOptions }) => void } */ + set(vatID, { options, bundle }) { + storage.set(`${vatID}.source`, stringify({ bundle })); + storage.set(`${vatID}.options`, stringify(options)); + }, + has(/** @type { unknown } */ vatID) { + return storage.has(`${vatID}.source`) && storage.has(`${vatID}.options`); + }, + }; + + /** + * @param {string} vatID + * @param {Bundle} bundle + * @param {ManagerOptions} options + */ + function initVatManager(vatID, bundle, options) { + assert( + !idToInitDetail.has(vatID), + X`vat with id ${q(vatID)} already initialized`, + ); + const { managerType } = options; + assert(managerType in factories, X`unknown managerType ${managerType}`); + + idToInitDetail.set(vatID, { bundle, options }); + + // TODO: add a way to remove a gatekeeper from ephemeral in kernel.js + // so that we can get rid of a vatKeeper when we evict its vat. + kernelKeeper.allocateVatKeeper(vatID); + } + + /** + * @param {string} vatID + * @returns { Promise } + */ + async function provideVatManager(vatID) { + const mgr = idToManager.get(vatID); + if (mgr) return mgr; + const detail = idToInitDetail.get(vatID); + assert(detail, X`no vat with ID ${q(vatID)} initialized`); + + // TODO: move kernelKeeper.allocateVatKeeper(vatID); + // to here + // TODO: load from snapshot + const { bundle, options } = detail; + const { managerType } = options; + + const translators = makeVatTranslators(vatID, kernelKeeper); + const vatSyscallHandler = buildVatSyscallHandler(vatID, translators); + + // console.log('provide: creating from bundle', vatID); + const manager = await factories[managerType].createFromBundle( + vatID, + bundle, + options, + vatSyscallHandler, + ); + + await manager.replayTranscript(); + idToManager.set(vatID, manager); + return manager; + } + + /** @type { (vatID: string) => Promise } */ + async function evict(vatID) { + assert( + idToInitDetail.has(vatID), + X`no vat with ID ${q(vatID)} initialized`, + ); + const mgr = idToManager.get(vatID); + if (!mgr) return; + idToManager.delete(vatID); + // console.log('evict: shutting down', vatID); + await mgr.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); + // already most recently used + if (pos + 1 === sizeHint) return; + if (pos >= 0) recent.splice(pos, 1); + recent.push(currentVatID); + // not yet full + if (recent.length <= sizeHint) return; + const [lru] = recent.splice(0, 1); + await evict(lru); + } + + /** @type {(vatID: string, d: VatDeliveryObject) => Promise } */ + async function deliverToVat(vatID, delivery) { + await applyAvailabilityPolicy(vatID); + return (await provideVatManager(vatID)).deliver(delivery); + } + + /** @type { (vatID: string) => Promise } */ + async function shutdown(vatID) { + await evict(vatID); + idToInitDetail.delete(vatID); + } + + return harden({ + // TODO: startup() method for start of kernel process + // see // instantiate all static vats + // in kernel.js + initVatManager, + deliverToVat, + + // mostly for testing? + activeVatIDs: () => [...idToManager.keys()], + + shutdown, // should this be shutdown for the whole thing? + }); +} diff --git a/packages/SwingSet/test/workers/test-vatwarehouse.js b/packages/SwingSet/test/workers/test-vatwarehouse.js index 7e2d5e87429..991ac484fa2 100644 --- a/packages/SwingSet/test/workers/test-vatwarehouse.js +++ b/packages/SwingSet/test/workers/test-vatwarehouse.js @@ -5,7 +5,7 @@ import { spawn } from 'child_process'; // eslint-disable-next-line import/order import { test } from '../../tools/prepare-test-env-ava'; -import { assert, q, details as d } from '@agoric/assert'; +import { assert } from '@agoric/assert'; import { initSwingStore } from '@agoric/swing-store-simple'; import bundleSource from '@agoric/bundle-source'; import { Remotable, getInterfaceOf } from '@agoric/marshal'; @@ -16,176 +16,9 @@ import { wrapStorage } from '../../src/kernel/state/storageWrapper'; import { loadBasedir } from '../../src'; import { makeStartXSnap } from '../../src/controller'; -import { makeVatTranslators } from '../../src/kernel/vatTranslator'; +import { makeVatWarehouse } from '../../src/kernel/vatManager/vat-warehouse'; -/** - * @param { ReturnType } kernelKeeper - * @param { SwingStore['storage'] } storage - * @param { Record } factories - * @param { (vatID: string, translators: unknown) => VatSyscallHandler } buildVatSyscallHandler - * @param {{ sizeHint?: number }=} policyOptions - * - * @typedef { ReturnType } SwingStore - * @typedef {(syscall: Tagged) => ['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, - storage, - factories, - buildVatSyscallHandler, - policyOptions, -) { - const { sizeHint = 10 } = policyOptions || {}; - /** @type { Map } */ - const idToManager = new Map(); - - const { parse, stringify } = JSON; - - // v$NN.source = JSON({ bundle }) or JSON({ bundleName }) - // v$NN.options = JSON - // TODO: check against createVatDynamically - // TODO transaction boundaries? - // ensure kernel doesn't start transaction until after it gets manager? - const idToInitDetail = { - /** @type { (vatID: string) => { vatID: string, bundle: Bundle, options: ManagerOptions } } */ - get(vatID) { - const { bundle } = parse(storage.get(`${vatID}.source`)); - assert(bundle, d`${q(vatID)}: bundleName not supported`); - - const options = parse(storage.get(`${vatID}.options`)); - // TODO: validate options - - return { vatID, bundle, options }; - }, - /** @type { (vatID: string, d: { bundle: Bundle, options: ManagerOptions }) => void } */ - set(vatID, { options, bundle }) { - storage.set(`${vatID}.source`, stringify({ bundle })); - storage.set(`${vatID}.options`, stringify(options)); - }, - has(/** @type { unknown } */ vatID) { - return storage.has(`${vatID}.source`) && storage.has(`${vatID}.options`); - }, - }; - - /** - * @param {string} vatID - * @param {Bundle} bundle - * @param {ManagerOptions} options - */ - function initVatManager(vatID, bundle, options) { - assert( - !idToInitDetail.has(vatID), - d`vat with id ${vatID} already initialized`, - ); - const { managerType } = options; - assert(managerType in factories, d`unknown managerType ${managerType}`); - - idToInitDetail.set(vatID, { bundle, options }); - - // TODO: add a way to remove a gatekeeper from ephemeral in kernel.js - // so that we can get rid of a vatKeeper when we evict its vat. - kernelKeeper.allocateVatKeeper(vatID); - } - - /** - * @param {string} vatID - * @returns { Promise } - */ - async function provideVatManager(vatID) { - const mgr = idToManager.get(vatID); - if (mgr) return mgr; - const detail = idToInitDetail.get(vatID); - assert(detail, d`no vat with ID ${vatID} initialized`); - - // TODO: move kernelKeeper.allocateVatKeeper(vatID); - // to here - - // TODO: load from snapshot - - const { bundle, options } = detail; - const { managerType } = options; - - const translators = makeVatTranslators(vatID, kernelKeeper); - const vatSyscallHandler = buildVatSyscallHandler(vatID, translators); - - // console.log('provide: creating from bundle', vatID); - const manager = await factories[managerType].createFromBundle( - vatID, - bundle, - options, - vatSyscallHandler, - ); - - await manager.replayTranscript(); - idToManager.set(vatID, manager); - return manager; - } - - /** @type { (vatID: string) => Promise } */ - async function evict(vatID) { - assert(idToInitDetail.has(vatID), d`no vat with ID ${vatID} initialized`); - const mgr = idToManager.get(vatID); - if (!mgr) return; - idToManager.delete(vatID); - // console.log('evict: shutting down', vatID); - await mgr.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); - // already most recently used - if (pos + 1 === sizeHint) return; - if (pos >= 0) recent.splice(pos, 1); - recent.push(currentVatID); - // not yet full - if (recent.length <= sizeHint) return; - const [lru] = recent.splice(0, 1); - await evict(lru); - } - - /** @type {(vatID: string, d: VatDeliveryObject) => Promise } */ - async function deliverToVat(vatID, delivery) { - await applyAvailabilityPolicy(vatID); - return (await provideVatManager(vatID)).deliver(delivery); - } - - /** @type { (vatID: string) => Promise } */ - async function shutdown(vatID) { - await evict(vatID); - idToInitDetail.delete(vatID); - } - - return harden({ - // TODO: startup() method for start of kernel process - // see // instantiate all static vats - // in kernel.js - - initVatManager, - deliverToVat, - - // mostly for testing? - activeVatIDs: () => [...idToManager.keys()], - - shutdown, // should this be shutdown for the whole thing? - }); -} +import '../../src/types'; function aStorageAndKeeper() { const { storage: hostStorage } = initSwingStore(undefined);