Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(swingset): add controller.terminateVat(vatID, reason) #9253

Merged
merged 1 commit into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/SwingSet/src/controller/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQui
import { makeGcAndFinalize } from '@agoric/internal/src/lib-nodejs/gc-and-finalize.js';
import { kslot, krefOf } from '@agoric/kmarshal';
import { insistStorageAPI } from '../lib/storageAPI.js';
import { insistCapData } from '../lib/capdata.js';
import {
buildKernelBundle,
swingsetIsInitialized,
Expand All @@ -33,6 +34,10 @@ import {
import { makeStartXSnap } from './startXSnap.js';
import { makeStartSubprocessWorkerNode } from './startNodeSubprocess.js';

/**
* @typedef { import('../types-internal.js').VatID } VatID
*/

const endoZipBase64Sha512Shape = harden({
moduleFormat: 'endoZipBase64',
endoZipBase64: M.string(harden({ stringLengthLimit: Infinity })),
Expand Down Expand Up @@ -441,6 +446,48 @@ export async function makeSwingsetController(
// no emitCrankHashes here because queueToVatRoot did that
return result;
},

/**
* terminate a vat by ID
*
* This allows the host app to terminate any vat. The effect is
* equivalent to the holder of the vat's `adminNode` calling
* `E(adminNode).terminateWithFailure(reason)`, or the vat itself
* calling `vatPowers.exitVatWithFailure(reason)`. It accepts a
* reason capdata structure (use 'kser()' to build one), which
* will be included in rejection data for the promise available to
* `E(adminNode).done()`, just like the internal termination APIs.
* Note that no slots/krefs are allowed in 'reason' when
* terminating the vat externally.
*
* This is a superpower available only from the host app, not from
* within vats, since `vatID` is merely a string and can be forged
* trivially. The host app is responsible for supplying the right
* vatID to kill, by looking at the database or logs (note that
* vats do not know their own vatID, and `controller.vatNameToID`
* only works for static vats, not dynamic).
*
* This will cause state changes in the swing-store (specifically
* marking the vat as terminated, and rejection all its
* outstanding promises), which must be committed before they will
* be durable. Either call `hostStorage.commit()` immediately
* after calling this, or call `controller.run()` and *then*
* `hostStorage.commit()` as you would normally do in response to
* other I/O or timer activity.
*
* The first `controller.run()` after this call will delete all
* the old vat's state at once, unless you use a
* [`runPolicy`](../docs/run-policy.md) to rate-limit cleanups.
*
* @param {VatID} vatID
* @param {SwingSetCapData} reasonCD
*/

terminateVat(vatID, reasonCD) {
insistCapData(reasonCD);
assert(reasonCD.slots.length === 0, 'no slots allowed in reason');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how strict we should be about assert patterns, but this is the convention AIUI:

Suggested change
assert(reasonCD.slots.length === 0, 'no slots allowed in reason');
reasonCD.slots.length === 0 || Fail`no slots allowed in reason`;

kernel.terminateVatExternally(vatID, reasonCD);
},
});

writeSlogObject({ type: 'kernel-init-finish' });
Expand Down
11 changes: 11 additions & 0 deletions packages/SwingSet/src/kernel/kernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2063,6 +2063,16 @@ export default function buildKernel(
hooks[hookName] = hook;
}

function terminateVatExternally(vatID, reasonCD) {
assert(started, 'must do kernel.start() before terminateVatExternally()');
insistCapData(reasonCD);
assert(reasonCD.slots.length === 0, 'no slots allowed in reason');
// this fires a promise when worker is dead, mostly for tests, so don't
// give it to external callers
void terminateVat(vatID, true, reasonCD);
console.log(`scheduled vatID ${vatID} for termination`);
}

const kernel = harden({
// these are meant for the controller
installBundle,
Expand Down Expand Up @@ -2138,6 +2148,7 @@ export default function buildKernel(
kpStatus,
kpResolution,
addDeviceHook,
terminateVatExternally,
});

return kernel;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Far, E } from '@endo/far';

export function buildRootObject() {
let vatAdmin;
let bcap;
let root;
let adminNode;
let exitval;

return Far('root', {
bootstrap: async (vats, devices) => {
vatAdmin = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin);
bcap = await E(vatAdmin).getNamedBundleCap('doomed');
const res = await E(vatAdmin).createVat(bcap);
root = res.root;
adminNode = res.adminNode;
E(adminNode)
.done()
.then(
happy => (exitval = ['fulfill', happy]),
sad => (exitval = ['reject', sad]),
);
},
ping: async count => E(root).ping(count),
getExitVal: () => exitval,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// eslint-disable-next-line import/order
import { test } from '../../tools/prepare-test-env-ava.js';

import { initSwingStore } from '@agoric/swing-store';
import { kser, kunser } from '@agoric/kmarshal';
import { initializeSwingset, makeSwingsetController } from '../../src/index.js';

const bfile = name => new URL(name, import.meta.url).pathname;

const testExternalTermination = async (t, defaultManagerType) => {
/** @type {SwingSetConfig} */
const config = {
defaultManagerType,
bootstrap: 'bootstrap',
vats: {
bootstrap: { sourceSpec: bfile('./bootstrap-external-termination.js') },
},
bundles: {
doomed: { sourceSpec: bfile('./vat-doomed.js') },
},
};

const kernelStorage = initSwingStore().kernelStorage;
await initializeSwingset(config, [], kernelStorage);
const c = await makeSwingsetController(kernelStorage);
t.teardown(c.shutdown);
c.pinVatRoot('bootstrap');
await c.run();

const getVatIDs = () => c.dump().vatTables.map(vt => vt.vatID);

// vat-doomed should now be running. We casually assume the new vat
// has the last ID
const vatIDs = getVatIDs();
const vatID = vatIDs[vatIDs.length - 1];

{
const kpid = c.queueToVatRoot('bootstrap', 'ping', [1]);
await c.run();
t.is(kunser(c.kpResolution(kpid)), 1);
}
{
const kpid = c.queueToVatRoot('bootstrap', 'getExitVal');
await c.run();
t.is(kunser(c.kpResolution(kpid)), undefined);
}

// The "vat has been terminated" flags are set synchronously during
// c.terminateVat(), as well as all the vat's promises being
// rejected. The deletion of state happens during the first cleanup
// crank, which (since we aren't limiting it with a runPolicy)
// cleans to completion during this c.run()

c.terminateVat(vatID, kser('doom!'));
await c.run();

t.false(getVatIDs().includes(vatID));

{
// this provokes noise: liveslots logs one RemoteError
const kpid = c.queueToVatRoot('bootstrap', 'ping', [1]);
await c.run();
t.is(c.kpStatus(kpid), 'rejected');
t.deepEqual(kunser(c.kpResolution(kpid)), Error('vat terminated'));
}

{
const kpid = c.queueToVatRoot('bootstrap', 'getExitVal');
await c.run();
t.deepEqual(kunser(c.kpResolution(kpid)), ['reject', 'doom!']);
}
};

test('external termination: local worker', async t => {
await testExternalTermination(t, 'local');
});

test('external termination: xsnap worker', async t => {
await testExternalTermination(t, 'xsnap');
});
7 changes: 7 additions & 0 deletions packages/SwingSet/test/external-termination/vat-doomed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Far } from '@endo/far';

export function buildRootObject() {
return Far('doomed', {
ping: count => count,
});
}
Loading