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

Vat-side promise ID retirement #2358

Merged
merged 1 commit into from
Feb 8, 2021
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
29 changes: 17 additions & 12 deletions packages/SwingSet/docs/vat-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@ If instead, the run-queue action is `notify`, the action will name a promise (wh
The `Delivery` object is a hardened Array of data (Objects, Arrays, and Strings, all of which can be serialized as JSON), whose first element is a type. It will take one of the following shapes:

* `['message', targetSlot, msg]`
* `['notify', vpid, vp]`
* `['notify', resolutions]`

In the `message` form, `targetSlot` is a object/promise reference (a string like `o+13` or `p-24`), which identifies the object or promise to which the message is being sent. This target can be a promise if the message is being pipelined to the result of some earlier message.

The `msg` field is an object shaped like `{ method, args, result }`, where `method` is a string (the method name being invoked), `args` is a "capdata" structure (an object `{ body, slots }`, where `body` is a JSON-formatted string, and `slots` is an array of object/promise references), and `result` is either `null` or a promise reference string (the promise that should be resolved by the executing vat if/when the message processing is complete).

In the `notify` form, `vpid` is a promise reference that names the promise being resolved. `vp` is a record that describes the resolution. It will take one of the following forms:
In the `notify` form, `resolutions` is an array of one or more resolution descriptors, each of which is an array of the form:

* `{ state: 'fulfilledToPresence', slot }` (where `slot` is an object reference string)
* `{ state: 'fulfilledToData', data }` (where `data` is a capdata structure)
* `{ state: 'rejected', data }` (again `data` is a capdata structure)
* (a future form might use `state: 'forwarded'`, but that is not implemented yet)
* `[vpid, promiseDescriptor]`

`vpid` is a promise reference that names the promise being resolved. `promiseDescriptor` is a record that describes the resolution, in the form:

* `{ rejected, data }`

`rejected` is a boolean value, where `true` indicates that the promise is being fulfilled and `false` indicates that `data` is a capdata structure that describes the value the promise is being fulfilled to or rejected as.

This Delivery object begins life in the kernel as a `KernelDelivery` object, in which all of the object/promise references ("slots") are kernel-centric. Object references will look like `ko123`, and promise references will look like `kp456`.

Expand All @@ -49,18 +52,20 @@ The VatManager is given access to a `VatSyscallHandler` function. This takes a `
* `['send', target, msg]`
* `['callNow', target, method, args]`
* `['subscribe', vpid]`
* `['fulfillToPresence', vpid, slot]`
* `['fulfillToData', vpid, data]`
* `['reject', vpid, data]`
* `['resolve', resolutions]`
* `['vatstoreGet', key]`
* `['vatstoreSet', key, data]`
* `['vatstoreDelete', key]`

As with deliveries (but in reverse), the translator converts this from vat-centric identifiers into kernel-centric ones, and emits a `KernelSyscall` object, with one of these forms:

* `['send', target, msg]`
* `['invoke', target, method, args]`
* `['subscribe', vatid, kpid]`
* `['fulfillToPresence', kpid, slot]`
* `['fulfillToData', kpid, data]`
* `['reject', kpid, data]`
* `['resolve', vatid, resolutions]`
* `['vatstoreGet', vatid, key]`
* `['vatstoreSet', vatid, key, data]`
* `['vatstoreDelete', vatid, key]`

The `KernelSyscallHandler` accepts one of these objects and executes the syscall. Most syscalls modify kernel state (by appending an item to the run-queue, or modifying promise- and object- tables) and then return an empty result. `invoke` is special in that it will synchronously invoke some device and then return a result that contains arbitrary data. In any event, the KernelSyscallHandler returns a `KernelSyscallResult` object, which has one of the following forms:

Expand Down
61 changes: 0 additions & 61 deletions packages/SwingSet/src/kernel/cleanup.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,6 @@
// import { kdebug } from './kdebug';
import { parseKernelSlot } from './parseKernelSlots';

// XXX temporary flags to control features during development
const ENABLE_PROMISE_ANALYSIS = true; // flag to enable/disable check to see if delete clist entry is ok

export function deleteCListEntryIfEasy(
vatID,
vatKeeper,
kernelKeeper,
kpid,
vpid,
kernelData,
) {
if (ENABLE_PROMISE_ANALYSIS) {
const visited = new Set();
let sawPromise;

function scanKernelPromise(scanKPID, scanKernelData) {
visited.add(scanKPID);
// kdebug(`@@@ scan ${scanKPID} ${JSON.stringify(scanKernelData)}`);
if (scanKernelData) {
for (const slot of scanKernelData.slots) {
const { type } = parseKernelSlot(slot);
if (type === 'promise') {
sawPromise = slot;
if (visited.has(slot)) {
// kdebug(`@@@ ${slot} previously visited`);
return true;
} else {
const { data } = kernelKeeper.getKernelPromise(slot);
// const { data, state } = kernelKeeper.getKernelPromise(slot);
if (data) {
if (scanKernelPromise(slot, data)) {
// kdebug(`@@@ scan ${slot} detects circularity`);
return true;
}
} else {
// kdebug(`@@@ scan ${slot} state = ${state}`);
}
}
}
}
}
// kdebug(`@@@ scan ${scanKPID} detects no circularity`);
return false;
}

// kdebug(`@@ checking ${vatID} ${kpid} for circularity`);
if (scanKernelPromise(kpid, kernelData)) {
// kdebug(
// `Unable to delete ${vatID} clist entry ${kpid}<=>${vpid} because it is indirectly self-referential`,
// );
return;
} else if (sawPromise) {
// kdebug(
// `Unable to delete ${vatID} clist entry ${kpid}<=>${vpid} because there was a contained promise ${sawPromise}`,
// );
return;
}
}
vatKeeper.deleteCListEntry(kpid, vpid);
}

export function getKpidsToRetire(kernelKeeper, rootKPID, rootKernelData) {
const seen = new Set();
function scanKernelPromise(kpid, kernelData) {
Expand Down
136 changes: 87 additions & 49 deletions packages/SwingSet/src/kernel/liveSlots.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,15 @@ function build(syscall, forVatID, cacheSize, vatPowers, vatParameters) {
return makeVatSlot('promise', true, promiseID);
}

const knownResolutions = new WeakMap();

function exportPromise(p) {
const pid = allocatePromiseID();
lsdebug(`Promise allocation ${forVatID}:${pid} in exportPromise`);
// eslint-disable-next-line no-use-before-define
p.then(thenResolve(pid), thenReject(pid));
if (!knownResolutions.has(p)) {
// eslint-disable-next-line no-use-before-define
p.then(thenResolve(p, pid), thenReject(p, pid));
}
return pid;
}

Expand Down Expand Up @@ -311,6 +315,49 @@ function build(syscall, forVatID, cacheSize, vatPowers, vatParameters) {
return val;
}

function resolutionCollector() {
const resolutions = [];
const doneResolutions = new Set();

function scanSlots(slots) {
for (const slot of slots) {
const { type } = parseVatSlot(slot);
if (type === 'promise') {
const p = slotToVal.get(slot);
assert(p, details`should have a value for ${slot} but didn't`);
const priorResolution = knownResolutions.get(p);
if (priorResolution && !doneResolutions.has(slot)) {
const [priorRejected, priorRes] = priorResolution;
// eslint-disable-next-line no-use-before-define
collect(slot, priorRejected, priorRes);
}
}
}
}

function collect(promiseID, rejected, value) {
doneResolutions.add(promiseID);
const valueSer = m.serialize(value);
resolutions.push([promiseID, rejected, valueSer]);
scanSlots(valueSer.slots);
}

function forPromise(promiseID, rejected, value) {
collect(promiseID, rejected, value);
return resolutions;
}

function forSlots(slots) {
scanSlots(slots);
return resolutions;
}

return {
forPromise,
forSlots,
};
}

function queueMessage(targetSlot, prop, args, returnedP) {
const serArgs = m.serialize(harden(args));
const resultVPID = allocatePromiseID();
Expand All @@ -326,6 +373,10 @@ function build(syscall, forVatID, cacheSize, vatPowers, vatParameters) {
)}) -> ${resultVPID}`,
);
syscall.send(targetSlot, prop, serArgs, resultVPID);
const resolutions = resolutionCollector().forSlots(serArgs.slots);
if (resolutions.length > 0) {
syscall.resolve(resolutions);
}

// ideally we'd wait until .then is called on p before subscribing, but
// the current Promise API doesn't give us a way to discover this, so we
Expand Down Expand Up @@ -410,16 +461,6 @@ function build(syscall, forVatID, cacheSize, vatPowers, vatParameters) {

const args = m.unserialize(argsdata);

let notifySuccess = () => undefined;
let notifyFailure = () => undefined;
if (result) {
insistVatType('promise', result);
// eslint-disable-next-line no-use-before-define
notifySuccess = thenResolve(result);
// eslint-disable-next-line no-use-before-define
notifyFailure = thenReject(result);
}

// If the method is missing, or is not a Function, or the method throws a
// synchronous exception, we notify the caller (by rejecting the result
// promise, if any). If the method returns an eventually-rejected
Expand All @@ -442,6 +483,15 @@ function build(syscall, forVatID, cacheSize, vatPowers, vatParameters) {
// TODO: untested, but in principle sound.
res = HandledPromise.get(t, method);
}
let notifySuccess = () => undefined;
let notifyFailure = () => undefined;
if (result) {
insistVatType('promise', result);
// eslint-disable-next-line no-use-before-define
notifySuccess = thenResolve(res, result);
// eslint-disable-next-line no-use-before-define
notifyFailure = thenReject(res, result);
}
res.then(notifySuccess, notifyFailure);
}

Expand All @@ -453,50 +503,38 @@ function build(syscall, forVatID, cacheSize, vatPowers, vatParameters) {
slotToVal.delete(promiseID);
}

const ENABLE_PROMISE_ANALYSIS = true;

function retirePromiseIDIfEasy(promiseID, data) {
if (ENABLE_PROMISE_ANALYSIS) {
for (const slot of data.slots) {
const { type } = parseVatSlot(slot);
if (type === 'promise') {
lsdebug(
`Unable to retire ${promiseID} because slot ${slot} is a promise`,
);
return;
}
}
}
retirePromiseID(promiseID);
}

function thenResolve(promiseID) {
function thenHandler(p, promiseID, rejected) {
insistVatType('promise', promiseID);
return res => {
harden(res);
lsdebug(`ls.thenResolve fired`, res);
const ser = m.serialize(res);
syscall.resolve([[promiseID, false, ser]]);
return value => {
knownResolutions.set(p, harden([rejected, value]));
harden(value);
FUDCo marked this conversation as resolved.
Show resolved Hide resolved
lsdebug(`ls.thenHandler fired`, value);
const resolutions = resolutionCollector().forPromise(
promiseID,
rejected,
value,
);

syscall.resolve(resolutions);

const pRec = importedPromisesByPromiseID.get(promiseID);
if (pRec) {
pRec.resolve(res);
if (rejected) {
pRec.reject(value);
} else {
pRec.resolve(value);
}
}
retirePromiseIDIfEasy(promiseID, ser);
retirePromiseID(promiseID);
};
}

function thenReject(promiseID) {
return rej => {
harden(rej);
lsdebug(`ls thenReject fired`, rej);
const ser = m.serialize(rej);
syscall.resolve([[promiseID, true, ser]]);
const pRec = importedPromisesByPromiseID.get(promiseID);
if (pRec) {
pRec.reject(rej);
}
retirePromiseIDIfEasy(promiseID, ser);
};
function thenResolve(p, promiseID) {
return thenHandler(p, promiseID, false);
}

function thenReject(p, promiseID) {
return thenHandler(p, promiseID, true);
}

function notifyOnePromise(promiseID, rejected, data) {
Expand Down
16 changes: 3 additions & 13 deletions packages/SwingSet/src/kernel/vatTranslator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { insistKernelType, parseKernelSlot } from './parseKernelSlots';
import { insistVatType, parseVatSlot } from '../parseVatSlots';
import { insistCapData } from '../capdata';
import { kdebug, legibilizeMessageArgs, legibilizeValue } from './kdebug';
import { deleteCListEntryIfEasy } from './cleanup';

/*
* Return a function that converts KernelDelivery objects into VatDelivery
Expand Down Expand Up @@ -223,6 +222,7 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) {

function translateResolve(vresolutions) {
const kresolutions = [];
const kpidsResolved = [];
let idx = 0;
for (const resolution of vresolutions) {
const [vpid, rejected, data] = resolution;
Expand All @@ -238,19 +238,9 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) {
);
idx += 1;
kresolutions.push([kpid, rejected, kernelData]);
deleteCListEntryIfEasy(
vatID,
vatKeeper,
kernelKeeper,
kpid,
vpid,
kernelData,
);
kpidsResolved.push(kpid);
}
// XXX TODO Once we get rid of the "if easy" logic, the above deletions
// should be collected and then processed in a batch here after all the
// translation is done, e.g., something like:
// vatKeeper.deleteCListEntriesForKernelSlots(targets);
vatKeeper.deleteCListEntriesForKernelSlots(kpidsResolved);
return harden(['resolve', vatID, kresolutions]);
}

Expand Down
4 changes: 4 additions & 0 deletions packages/SwingSet/src/vats/comms/clist-kernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export function makeKernel(state, syscall, stateKit) {
// promise is retired (to remember the resolution).
assert(p, `how did I forget about ${vpid}`);

if (p.kernelAwaitingResolve) {
return vpid;
}

if (p.resolved) {
// The vpid might have been retired, in which case we must not use it
// when speaking to the kernel. It will only be retired if 1: it
Expand Down
5 changes: 5 additions & 0 deletions packages/SwingSet/src/vats/comms/delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function makeDeliveryKit(state, syscall, transmit, clistKit, stateKit) {
changeDeciderFromRemoteToComms,
getPromiseSubscribers,
markPromiseAsResolved,
markPromiseAsResolvedInKernel,
} = stateKit;

function mapDataToKernel(data) {
Expand Down Expand Up @@ -340,6 +341,10 @@ export function makeDeliveryKit(state, syscall, transmit, clistKit, stateKit) {
// promiseTable reminds provideKernelForLocal to use a fresh VPID if we
// ever reference it again in the future
}
for (const resolution of resolutions) {
const [vpid] = resolution;
markPromiseAsResolvedInKernel(vpid);
}
}

function resolveToRemote(remoteID, resolutions) {
Expand Down
Loading