Skip to content

Commit

Permalink
feat(swingset): activate metering of dynamic vats
Browse files Browse the repository at this point in the history
vatManager now does a check, just after the vat finishes with a crank, to see
if a meter was active for that vat (`meterRecord`). If so, it checks the
meter for exhaustion and uses `notifyTermination` to tell the admin facet
about it. Otherwise it refills the meter. Vats get unlimited cranks, but each
crank has a limited meter budget.

The dynamic-vat creation code makes a new meterRecord, applies the
metering-transform (and endowment to let the transformed code do `getMeter`),
and prepares a `notifyTermination` callback which will queue a message to the
vatAdminWrapper with the details.

The vatAdminWrapper now manages an additional `done` promise for each dynamic
vat it manages. When it gets the `notifyTermination` message, it resolves
this promise. Whichever vat object holds the `adminNode` can retrieve this
promise and get notified when the vat dies.

The unit test exercises a vat overrunning the "allocate" meter (which is easy
to trigger without a long slow loop), and makes sure the vat doesn't respond
to future messages after it's been exhausted. The existing dynamic-vat
test (vat-admin/test-innerVat.js) was updated to install global metering,
because now all dynamic vats are metered, whether the test is exercising
metering or not, and swingset emits a warning unless global metering is
installed.

We don't currently attempt to clean up the vat in any way. We rely upon the
vat's one meter remaining exhausted and never being refilled. As long as that
remains the case, all vat code will throw (the same exception) immediately
upon any message delivery, so while the vat's liveslots code gets to run (and
result promises are rejected appropriately), the vat code itself will never
get control again. The `notifyTermination` callback will be fired each time,
but it has an internal flag to ignore the later ones.

We also don't yet have a way to preemptively terminate a vat. We could add
this pretty easily by changing `doProcess` to check a new "null or Error"
flag just before dispatching into the vat.

One missing feature is that any Promises the late vat was controlling will
remain forever unresolved. Ideally all those promises should be rejected as
soon as the vat is known to be terminated. This is the highest-priority
followup task.

A further-out task is to delete the vat from memory, decref its c-list
entries, and somehow propagate disconnect messages back to holders of
now-dangling object references.

Another remaining task is to handle state cleanup and "rewind the
transaction" better. In the present code, when a vat dies mid-way through a
crank, any syscalls it made before the meter exhausted will still get
through. External observers will see a prefix of the messages the vat would
have sent if it hadn't run out of metering budget. Ideally the entire crank
would atomically happen or not happen, which will require us to buffer those
syscalls until the crank finishes with time still on the clock. This is
visible now, but isn't too serious yet. It will become more important when we
consider allowing vats to re-start the message that killed them (using a
bigger meter).
  • Loading branch information
warner committed Jun 29, 2020
1 parent 1a363d2 commit 96eb63f
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 7 deletions.
44 changes: 41 additions & 3 deletions packages/SwingSet/src/kernel/dynamicVat.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,57 @@ export function makeDynamicVatCreator(stuff) {
function createVatDynamically(vatSourceBundle) {
const vatID = allocateUnusedVatID();

const meterRecord = null;
const notifyTermination = null;
// fail-stop: we refill the meter after each crank (in vatManager
// doProcess()), but if the vat exhausts its meter within a single crank,
// it will never run again. We set refillEachCrank:false because we want
// doProcess to do the refilling itself, so it can count the usage
const meterRecord = makeGetMeter({
refillEachCrank: false,
refillIfExhausted: false,
});

let terminated = false;

function notifyTermination(error) {
if (terminated) {
return;
}
terminated = true;
const vatAdminVatId = vatNameToID('vatAdmin');
const vatAdminRootObjectSlot = makeVatRootObjectSlot();

const args = {
body: JSON.stringify([
vatID,
error
? { '@qclass': 'error', name: error.name, message: error.message }
: { '@qclass': 'undefined' },
]),
slots: [],
};

queueToExport(
vatAdminVatId,
vatAdminRootObjectSlot,
'vatTerminated',
args,
'logFailure',
);
}

async function makeBuildRootObject() {
if (typeof vatSourceBundle !== 'object') {
throw Error(
`createVatDynamically() requires bundle, not a plain string`,
);
}
const getMeter = meterRecord.getMeter;
const transforms = [src => transformMetering(src, getMeter)];

const vatNS = await importBundle(vatSourceBundle, {
filePrefix: vatID,
endowments: vatEndowments,
transforms,
endowments: { ...vatEndowments, getMeter },
});
// TODO: use a named export, not default
const buildRootObject = vatNS.default;
Expand Down
21 changes: 21 additions & 0 deletions packages/SwingSet/src/kernel/vatAdmin/vatAdminWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function producePRR() {
export default function setup(syscall, state, helpers) {
function build(E, D) {
const pending = new Map(); // vatID -> { resolve, reject } for promise
const running = new Map(); // vatID -> { resolve, reject } for doneP

function createVatAdminService(vatAdminNode) {
return harden({
Expand All @@ -25,6 +26,9 @@ export default function setup(syscall, state, helpers) {
const [promise, pendingRR] = producePRR();
pending.set(vatID, pendingRR);

const [doneP, doneRR] = producePRR();
running.set(vatID, doneRR);

const adminNode = harden({
terminate() {
D(vatAdminNode).terminate(vatID);
Expand All @@ -33,6 +37,9 @@ export default function setup(syscall, state, helpers) {
adminData() {
return D(vatAdminNode).adminStats(vatID);
},
done() {
return doneP;
},
});
return promise.then(root => {
return { adminNode, root };
Expand All @@ -52,9 +59,23 @@ export default function setup(syscall, state, helpers) {
}
}

// the kernel sends this when the vat halts
function vatTerminated(vatID, error) {
// 'error' is undefined if adminNode.terminate() killed it, else it
// will be a RangeError from a metering fault
const { resolve, reject } = running.get(vatID);
running.delete(vatID);
if (error) {
reject(error);
} else {
resolve();
}
}

return harden({
createVatAdminService,
newVatCallback,
vatTerminated,
});
}

Expand Down
23 changes: 20 additions & 3 deletions packages/SwingSet/src/kernel/vatManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,16 +342,33 @@ export default function makeVatManager(
return waitUntilQuiescent();
}

function updateStats(_used) {
// TODO: accumulate used.allocate and used.compute into vatStats
}

async function doProcess(dispatchRecord, errmsg) {
const dispatchOp = dispatchRecord[0];
const dispatchArgs = dispatchRecord.slice(1);
transcriptStartDispatch(dispatchRecord);
await runAndWait(() => dispatch[dispatchOp](...dispatchArgs), errmsg);
// TODO: if the vat is metered, and requested death-before-confusion,
// then find the relevant meter, check whether it's exhausted, and react
// somehow
stopGlobalMeter();

// refill this vat's meter, if any, accumulating its usage for stats
if (meterRecord) {
// note that refill() won't actually refill an exhausted meter
const used = meterRecord.refill();
const exhaustionError = meterRecord.isExhausted();
if (exhaustionError) {
// TODO: if the vat requested death-before-confusion, unwind this
// crank and pretend all its syscalls never happened
if (notifyTermination) {
notifyTermination(exhaustionError);
}
} else {
updateStats(used);
}
}

// refill all within-vat -created meters
refillAllMeters();

Expand Down
16 changes: 16 additions & 0 deletions packages/SwingSet/test/metering/metered-dynamic-vat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import harden from '@agoric/harden';
import meterMe from './metered-code';

export default function buildRoot(_dynamicVatPowers) {
return harden({
async run() {
meterMe([], 'no');
return 42;
},

async explode(how) {
meterMe([], how);
return -1;
},
});
}
73 changes: 73 additions & 0 deletions packages/SwingSet/test/metering/test-dynamic-vat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* global harden */
import '@agoric/install-metering-and-ses';
import bundleSource from '@agoric/bundle-source';
import tap from 'tap';
import { buildVatController } from '../../src/index';
import makeNextLog from '../make-nextlog';

function capdata(body, slots = []) {
return harden({ body, slots });
}

function capargs(args, slots = []) {
return capdata(JSON.stringify(args), slots);
}

tap.test('metering dynamic vats', async t => {
// we'll give this bundle to the loader vat, which will use it to create a
// new (metered) dynamic vat
const dynamicVatBundle = await bundleSource(
require.resolve('./metered-dynamic-vat.js'),
);
const config = {
vats: new Map(),
bootstrapIndexJS: require.resolve('./vat-load-dynamic.js'),
};
const c = await buildVatController(config, true, []);
const nextLog = makeNextLog(c);

// let the vatAdminService get wired up before we create any new vats
await c.run();

// 'createVat' will import the bundle
c.queueToVatExport(
'_bootstrap',
'o+0',
'createVat',
capargs([dynamicVatBundle]),
);
await c.run();
t.deepEqual(nextLog(), ['created'], 'first create');

// First, send a message to the dynamic vat that runs normally
c.queueToVatExport('_bootstrap', 'o+0', 'run', capargs([]));
await c.run();

t.deepEqual(nextLog(), ['did run'], 'first run ok');

// Now send a message that makes the dynamic vat exhaust its meter. The
// message result promise should be rejected, and the control facet should
// report the vat's demise
c.queueToVatExport('_bootstrap', 'o+0', 'explode', capargs(['allocate']));
await c.run();

t.deepEqual(
nextLog(),
[
'did explode: RangeError: Allocate meter exceeded',
'terminated: RangeError: Allocate meter exceeded',
],
'first boom',
);

// the dead vat should stay dead
c.queueToVatExport('_bootstrap', 'o+0', 'run', capargs([]));
await c.run();
t.deepEqual(
nextLog(),
['run exploded: RangeError: Allocate meter exceeded'],
'stay dead',
);

t.end();
});
54 changes: 54 additions & 0 deletions packages/SwingSet/test/metering/vat-load-dynamic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import harden from '@agoric/harden';
import { E } from '@agoric/eventual-send';

function build(buildStuff) {
const { log } = buildStuff;
let service;
let control;

return harden({
async bootstrap(argv, vats, devices) {
service = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin);
},

async createVat(bundle) {
control = await E(service).createVat(bundle);
E(control.adminNode)
.done()
.then(
() => log('finished'),
err => log(`terminated: ${err}`),
);
log(`created`);
},

async run() {
try {
await E(control.root).run();
log('did run');
} catch (err) {
log(`run exploded: ${err}`);
}
},

async explode(how) {
try {
await E(control.root).explode(how);
log('failed to explode');
} catch (err) {
log(`did explode: ${err}`);
}
},
});
}

export default function setup(syscall, state, helpers, vatPowers0) {
const { log, makeLiveSlots } = helpers;
return makeLiveSlots(
syscall,
state,
(_E, _D, _vatPowers) => build({ log }),
helpers.vatID,
vatPowers0,
);
}
2 changes: 1 addition & 1 deletion packages/SwingSet/test/vat-admin/test-innerVat.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import '@agoric/install-ses';
import '@agoric/install-metering-and-ses';
import path from 'path';
import { test } from 'tape';
import { initSwingStore } from '@agoric/swing-store-simple';
Expand Down

0 comments on commit 96eb63f

Please sign in to comment.