Skip to content

Commit

Permalink
Deep stacks for E.when and for handled async operations (#1987)
Browse files Browse the repository at this point in the history
  • Loading branch information
erights authored Nov 21, 2020
1 parent 9d09490 commit fb40b32
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 4 deletions.
4 changes: 3 additions & 1 deletion packages/assert/src/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import './types';

const { freeze } = Object;

/** @type {Assert} */
const globalAssert = globalThis.assert;

Expand Down Expand Up @@ -66,5 +68,5 @@ function an(str) {
}
return `a ${str}`;
}
harden(an);
freeze(an);
export { an };
1 change: 1 addition & 0 deletions packages/eventual-send/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"homepage": "https://github.com/Agoric/agoric-sdk#readme",
"devDependencies": {
"@agoric/assert": "^0.1.0",
"@agoric/install-ses": "^0.4.0",
"ava": "^3.12.1",
"esm": "^3.2.7",
Expand Down
8 changes: 6 additions & 2 deletions packages/eventual-send/src/E.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* global harden */

import { trackTurns } from './track-turns';

// eslint-disable-next-line spaced-comment
/// <reference path="index.d.ts" />

Expand Down Expand Up @@ -96,8 +98,10 @@ export default function makeE(HandledPromise) {
return harden(new Proxy(() => {}, handler));
};

E.when = (x, onfulfilled = undefined, onrejected = undefined) =>
HandledPromise.resolve(x).then(onfulfilled, onrejected);
E.when = (x, onfulfilled = undefined, onrejected = undefined) => {
const [onsuccess, onfailure] = trackTurns([onfulfilled, onrejected]);
return HandledPromise.resolve(x).then(onsuccess, onfailure);
};

return harden(E);
}
7 changes: 6 additions & 1 deletion packages/eventual-send/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* global harden */

import { trackTurns } from './track-turns';

const {
defineProperties,
getOwnPropertyDescriptors,
Expand Down Expand Up @@ -389,6 +391,9 @@ export function makeHandledPromise() {

handle = (p, operation, ...opArgs) => {
ensureMaps();
// eslint-disable-next-line no-use-before-define
const doIt = (handler, o) => handler[operation](o, ...opArgs, returnedP);
const [trackedDoIt] = trackTurns([doIt]);
const returnedP = new HandledPromise((resolve, reject) => {
// We run in a future turn to prevent synchronous attacks,
let raceIsOver = false;
Expand All @@ -400,7 +405,7 @@ export function makeHandledPromise() {
throw TypeError(`${handlerName}.${operation} is not a function`);
}
try {
resolve(handler[operation](o, ...opArgs, returnedP));
resolve(trackedDoIt(handler, o));
} catch (reason) {
reject(reason);
}
Expand Down
70 changes: 70 additions & 0 deletions packages/eventual-send/src/track-turns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @ts-nocheck
/* global assert globalThis */

// NOTE: We can't import these because they're not in scope before lockdown.
// import { assert, details as d } from '@agoric/assert';

// WARNING: Global Mutable State!
// This state is communicated to `assert` that makes it available to the
// causal console, which affects the console log output. Normally we
// regard the ability to see console log output as a meta-level privilege
// analogous to the ability to debug. Aside from that, this module should
// not have any observably mutable state.

let hiddenPriorError;
let hiddenCurrentTurn = 0;
let hiddenCurrentEvent = 0;

/**
* @typedef {((...args: any[]) => any) | void} TurnStarterFn
* An optional function that is not this-sensitive, expected to be called at
* bottom of stack to start a new turn.
*/

/**
* Given a list of `TurnStarterFn`s, returns a list of `TurnStarterFn`s whose
* `this`-free call behaviors are not observably different to those that
* cannot see console output. The only purpose is to cause additional
* information to appear on the console.
*
* The call to `trackTurns` is itself a sending event, that occurs in some call
* stack in some turn number at some event number within that turn. Each call
* to any of the returned `TurnStartFn`s is a receiving event that begins a new
* turn. This sending event caused each of those receiving events.
*
* @param {TurnStarterFn[]} funcs
* @returns {TurnStarterFn[]}
*/
export const trackTurns = funcs => {
if (typeof globalThis === 'undefined' || !globalThis.assert) {
return funcs;
}
hiddenCurrentEvent += 1;
const sendingError = new Error(
`Event: ${hiddenCurrentTurn}.${hiddenCurrentEvent}`,
);
if (hiddenPriorError !== undefined) {
assert.note(sendingError, assert.details`Caused by: ${hiddenPriorError}`);
}

return funcs.map(
func =>
func &&
((...args) => {
hiddenPriorError = sendingError;
hiddenCurrentTurn += 1;
hiddenCurrentEvent = 0;
try {
return func(...args);
} catch (err) {
assert.note(
err,
assert.details`Thrown from: ${hiddenPriorError}:${hiddenCurrentTurn}.${hiddenCurrentEvent}`,
);
throw err;
} finally {
hiddenPriorError = undefined;
}
}),
);
};
30 changes: 30 additions & 0 deletions packages/eventual-send/test/test-deep-send.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// This file does not start with "test-" because it is useful as an
// automated test. Rather, its purpose is just to run it to see what a
// deep stack looks like.

import '@agoric/install-ses';
import test from 'ava';
import { assert } from '@agoric/assert';
import { E } from './get-hp';

const { freeze } = Object;

const carol = freeze({
bar: () => assert.fail('Wut?'),
});

const bob = freeze({
foo: carolP => E(carolP).bar(),
});

const alice = freeze({
test: () => E(bob).foo(carol),
});

test('deep-stacks E', t => {
const q = alice.test();
return q.catch(reason => {
t.assert(reason instanceof Error);
console.log('expected failure', reason);
});
});
19 changes: 19 additions & 0 deletions packages/eventual-send/test/test-deep-stacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file does not start with "test-" because it is useful as an
// automated test. Rather, its purpose is just to run it to see what a
// deep stack looks like.

import '@agoric/install-ses';
import test from 'ava';
import { assert } from '@agoric/assert';
import { E } from './get-hp';

test('deep-stacks when', t => {
let r;
const p = new Promise(res => (r = res));
const q = E.when(p, v1 => E.when(v1 + 1, v2 => assert.equal(v2, 22)));
r(33);
return q.catch(reason => {
t.assert(reason instanceof Error);
console.log('expected failure', reason);
});
});

0 comments on commit fb40b32

Please sign in to comment.