-
Notifications
You must be signed in to change notification settings - Fork 231
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(async-flow): add more context to vow rejection #10359
base: master
Are you sure you want to change the base?
Changes from all commits
fc58f8d
588d227
5bd083a
1d9475a
733042d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,28 @@ | ||
/* eslint-disable no-use-before-define */ | ||
import { isVow } from '@agoric/vow/src/vow-utils.js'; | ||
import { isVow, toPassableCap } from '@agoric/vow/src/vow-utils.js'; | ||
import { heapVowE } from '@agoric/vow/vat.js'; | ||
import { throwLabeled } from '@endo/common/throw-labeled.js'; | ||
import { Fail, X, b, makeError, q } from '@endo/errors'; | ||
import { Fail, X, annotateError, b, makeError, q } from '@endo/errors'; | ||
import { E } from '@endo/eventual-send'; | ||
import { getMethodNames } from '@endo/eventual-send/utils.js'; | ||
import { Far, Remotable, getInterfaceOf } from '@endo/pass-style'; | ||
import { makeConvertKit } from './convert.js'; | ||
import { makeEquate } from './equate.js'; | ||
import { | ||
inlineTemplateArgs, | ||
hostCallToTemplateArgs, | ||
idTemplateTag, | ||
} from './template.js'; | ||
|
||
/** | ||
* @import {PromiseKit} from '@endo/promise-kit'; | ||
* @import {RemotableBrand} from '@endo/eventual-send'; | ||
* @import {Callable, Passable, PassableCap} from '@endo/pass-style'; | ||
* @import {Vow, VowTools, VowKit} from '@agoric/vow'; | ||
* @import {WeakMapStore} from '@agoric/store'; | ||
* @import {LogStore} from '../src/log-store.js'; | ||
* @import {Bijection} from '../src/bijection.js'; | ||
* @import {Host, HostVow, LogEntry, Outcome} from '../src/types.js'; | ||
* @import {Host, HostCall, HostVow, LogEntry, Outcome} from '../src/types.js'; | ||
*/ | ||
|
||
const { fromEntries, defineProperties, assign } = Object; | ||
|
@@ -28,6 +34,9 @@ const { fromEntries, defineProperties, assign } = Object; | |
* @param {VowTools} arg.vowTools | ||
* @param {(vowish: Promise | Vow) => void} arg.watchWake | ||
* @param {(problem: Error) => never} arg.panic | ||
* @param {string | Error} [arg.definitionStack] | ||
* @param {string} arg.tag | ||
* @param {WeakMapStore<PassableCap, HostCall>} [arg.hostVowToCall] | ||
*/ | ||
export const makeReplayMembrane = arg => { | ||
const noDunderArg = /** @type {typeof arg} */ ( | ||
|
@@ -43,6 +52,9 @@ export const makeReplayMembrane = arg => { | |
* @param {VowTools} arg.vowTools | ||
* @param {(vowish: Promise | Vow) => void} arg.watchWake | ||
* @param {(problem: Error) => never} arg.panic | ||
* @param {string | Error} [arg.definitionStack] | ||
* @param {string} arg.tag | ||
* @param {WeakMapStore<PassableCap, HostCall>} [arg.hostVowToCall] | ||
* @param {boolean} [arg.__eventualSendForTesting] CAVEAT: Only for async-flow tests | ||
*/ | ||
export const makeReplayMembraneForTesting = ({ | ||
|
@@ -51,6 +63,9 @@ export const makeReplayMembraneForTesting = ({ | |
vowTools, | ||
watchWake, | ||
panic, | ||
definitionStack, | ||
tag, | ||
hostVowToCall, | ||
__eventualSendForTesting, | ||
}) => { | ||
const { when, makeVowKit } = vowTools; | ||
|
@@ -70,6 +85,26 @@ export const makeReplayMembraneForTesting = ({ | |
Fail`generation expected non-negative; got ${generation}`; | ||
}; | ||
|
||
const flowDescription = definitionStack | ||
? idTemplateTag`${tag} defined at ${definitionStack}` | ||
: tag; | ||
|
||
/** | ||
* @param {Vow} vow | ||
* @param {HostCall} hostCall | ||
*/ | ||
const initHostCall = (vow, hostCall) => { | ||
harden(hostCall); | ||
if (!hostVowToCall) { | ||
return; | ||
} | ||
const key = toPassableCap(vow); | ||
if (hostVowToCall.has(key)) { | ||
return; | ||
} | ||
hostVowToCall.init(key, hostCall); | ||
}; | ||
|
||
// ////////////// Host or Interpreter to Guest /////////////////////////////// | ||
|
||
/** | ||
|
@@ -106,6 +141,25 @@ export const makeReplayMembraneForTesting = ({ | |
Fail`doReject should only be called on a registered unresolved promise`; | ||
} | ||
const guestReason = hostToGuest(hostReason); | ||
if (guestReason instanceof Error) { | ||
annotateError( | ||
guestReason, | ||
X(...inlineTemplateArgs`from flow ${flowDescription}`), | ||
); | ||
if (hostVowToCall) { | ||
const hostVowCap = toPassableCap(hostVow); | ||
if (hostVowToCall.has(hostVowCap)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In what case would the map not have the cap? |
||
const hostCall = hostVowToCall.get(hostVowCap); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to leave the host call in the map after rejection? I think doFulfill should remove from the map as well. |
||
const hostDescription = hostCallToTemplateArgs(hostCall); | ||
annotateError( | ||
guestReason, | ||
X( | ||
...inlineTemplateArgs`host rejection from call to ${hostDescription}`, | ||
), | ||
); | ||
} | ||
} | ||
} | ||
status.reject(guestReason); | ||
guestPromiseMap.set(guestPromise, 'settled'); | ||
}; | ||
|
@@ -157,9 +211,12 @@ export const makeReplayMembraneForTesting = ({ | |
const performCall = (hostTarget, optVerb, hostArgs, callIndex) => { | ||
let hostResult; | ||
try { | ||
hostResult = optVerb | ||
? hostTarget[optVerb](...hostArgs) | ||
: hostTarget(...hostArgs); | ||
hostResult = | ||
optVerb === undefined | ||
? hostTarget(...hostArgs) | ||
: hostTarget[optVerb](...hostArgs); | ||
isVow(hostResult) && | ||
initHostCall(hostResult, { target: hostTarget, method: optVerb }); | ||
Comment on lines
+218
to
+219
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should be able to do this on the guest side with the guest promise, right? Or possibly the |
||
// Try converting here just to route the error correctly | ||
hostToGuest(hostResult, `converting ${optVerb || 'host'} result`); | ||
} catch (hostProblem) { | ||
|
@@ -285,6 +342,11 @@ export const makeReplayMembraneForTesting = ({ | |
throw Panic`internal: eventual send synchronously failed ${hostProblem}`; | ||
} | ||
try { | ||
initHostCall(vow, { | ||
target: hostTarget, | ||
method: optVerb, | ||
eventual: true, | ||
}); | ||
Comment on lines
+345
to
+349
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah with eventual send it's gonna be more difficult to record on the guest side since we don't see the guest anymore. |
||
/** @type {LogEntry} */ | ||
const entry = harden(['doReturn', callIndex, vow]); | ||
log.pushEntry(entry); | ||
|
@@ -568,32 +630,35 @@ export const makeReplayMembraneForTesting = ({ | |
hVow, | ||
async hostFulfillment => { | ||
await log.promiseReplayDone(); // should never reject | ||
if (!stopped && guestPromiseMap.get(promiseKey) !== 'settled') { | ||
/** @type {LogEntry} */ | ||
const entry = harden(['doFulfill', hVow, hostFulfillment]); | ||
log.pushEntry(entry); | ||
try { | ||
interpretOne(topDispatch, entry); | ||
} catch { | ||
// interpretOne does its own try/catch/panic, so failure would | ||
// already be registered. Here, just return to avoid the | ||
// Unhandled rejection. | ||
} | ||
if (stopped || guestPromiseMap.get(promiseKey) === 'settled') { | ||
return; | ||
} | ||
/** @type {LogEntry} */ | ||
const entry = harden(['doFulfill', hVow, hostFulfillment]); | ||
log.pushEntry(entry); | ||
try { | ||
interpretOne(topDispatch, entry); | ||
} catch { | ||
// interpretOne does its own try/catch/panic, so failure would | ||
// already be registered. Here, just return to avoid the | ||
// Unhandled rejection. | ||
} | ||
}, | ||
async hostReason => { | ||
await log.promiseReplayDone(); // should never reject | ||
if (!stopped && guestPromiseMap.get(promiseKey) !== 'settled') { | ||
/** @type {LogEntry} */ | ||
const entry = harden(['doReject', hVow, hostReason]); | ||
log.pushEntry(entry); | ||
try { | ||
interpretOne(topDispatch, entry); | ||
} catch { | ||
// interpretOne does its own try/catch/panic, so failure would | ||
// already be registered. Here, just return to avoid the | ||
// Unhandled rejection. | ||
} | ||
if (stopped || guestPromiseMap.get(promiseKey) === 'settled') { | ||
return; | ||
} | ||
|
||
/** @type {LogEntry} */ | ||
const entry = harden(['doReject', hVow, hostReason]); | ||
log.pushEntry(entry); | ||
try { | ||
interpretOne(topDispatch, entry); | ||
} catch { | ||
// interpretOne does its own try/catch/panic, so failure would | ||
// already be registered. Here, just return to avoid the | ||
// Unhandled rejection. | ||
} | ||
}, | ||
); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is all very neat, but I think I'd grok it better with some unit tests showing what each helper does. |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,130 @@ | ||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* @import {HostCall} from './types.js'; | ||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const { assign, defineProperty } = Object; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** Doesn't need to be exhaustive, just a little prettier than JSON-quoting. */ | ||||||||||||||||||||||||
const BEST_GUESS_ID_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* Return an object that mimics a template strings array. | ||||||||||||||||||||||||
* | ||||||||||||||||||||||||
* @param {string[]} strings | ||||||||||||||||||||||||
* @param {string[]} [raw] optional raw strings, defaults to `strings` | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||||||||||||||||||||||||
* @returns {TemplateStringsArray} | ||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||
export const makeTemplateStringsArray = (strings, raw = strings) => { | ||||||||||||||||||||||||
// Get the shape of the result object to match TemplateStringsArray. | ||||||||||||||||||||||||
const result = assign([...strings], { raw }); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Make the raw property non-enumerable. | ||||||||||||||||||||||||
defineProperty(result, 'raw', { | ||||||||||||||||||||||||
value: raw, | ||||||||||||||||||||||||
enumerable: false, | ||||||||||||||||||||||||
}); | ||||||||||||||||||||||||
Comment on lines
+19
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
return harden(result); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
harden(makeTemplateStringsArray); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* When used as a template tag, this function returns its arguments verbatim. | ||||||||||||||||||||||||
* | ||||||||||||||||||||||||
* @template {any[]} A | ||||||||||||||||||||||||
* @param {A} allArgs | ||||||||||||||||||||||||
erights marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
* @returns {A} | ||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||
export const idTemplateTag = (...allArgs) => allArgs; | ||||||||||||||||||||||||
harden(idTemplateTag); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* Convert a replay membrane HostCall structure to template arguments. | ||||||||||||||||||||||||
* | ||||||||||||||||||||||||
* @param {HostCall} hostCall | ||||||||||||||||||||||||
* @returns {[TemplateStringsArray, ...any[]]} | ||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||
export const hostCallToTemplateArgs = ({ target, method, eventual }) => { | ||||||||||||||||||||||||
/** @type {string[]} */ | ||||||||||||||||||||||||
const tmpl = []; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** @type {any[]} */ | ||||||||||||||||||||||||
const args = []; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const tpush = str => { | ||||||||||||||||||||||||
tmpl.push(str); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
const tappend = str => { | ||||||||||||||||||||||||
tmpl[tmpl.length - 1] += str; | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
tpush(eventual ? 'E' : ''); | ||||||||||||||||||||||||
tappend('('); | ||||||||||||||||||||||||
args.push(target); | ||||||||||||||||||||||||
tpush(')'); | ||||||||||||||||||||||||
if (typeof method === 'string') { | ||||||||||||||||||||||||
if (BEST_GUESS_ID_REGEX.test(method)) { | ||||||||||||||||||||||||
tappend(`.${method}`); | ||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||
tappend(`[${JSON.stringify(method)}]`); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} else if (method !== undefined) { | ||||||||||||||||||||||||
tappend(`[${String(method)}]`); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
tappend('(...)'); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return /** @type {const} */ ([makeTemplateStringsArray(tmpl), ...args]); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
harden(hostCallToTemplateArgs); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/** | ||||||||||||||||||||||||
* Template tag to flatten any nested template arguments by joining them to the | ||||||||||||||||||||||||
* returned template strings and rest arguments. | ||||||||||||||||||||||||
* | ||||||||||||||||||||||||
* @param {TemplateStringsArray} tmpl | ||||||||||||||||||||||||
* @param {any[]} args | ||||||||||||||||||||||||
* @returns {[TemplateStringsArray, ...any[]]} | ||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||
export const inlineTemplateArgs = (tmpl, ...args) => { | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering if the ergonomics wouldn't be simpler with a wrapper for the original template tag function: inlineArgs(X)`foo bar ${inlinedArg}`; |
||||||||||||||||||||||||
/** @type {string[]} */ | ||||||||||||||||||||||||
const itmpl = [tmpl[0]]; | ||||||||||||||||||||||||
/** @type {any[]} */ | ||||||||||||||||||||||||
const iargs = []; | ||||||||||||||||||||||||
for (let i = 0; i < args.length; i += 1) { | ||||||||||||||||||||||||
const arg = args[i]; | ||||||||||||||||||||||||
const nextStr = tmpl[i + 1]; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Could be a template and argument list. | ||||||||||||||||||||||||
const argLength = Array.isArray(arg) ? arg.length : 0; | ||||||||||||||||||||||||
/** @type {TemplateStringsArray | undefined} */ | ||||||||||||||||||||||||
const t = argLength && arg[0] ? arg[0] : undefined; | ||||||||||||||||||||||||
if ( | ||||||||||||||||||||||||
!Array.isArray(t) || | ||||||||||||||||||||||||
!Array.isArray(t.raw) || | ||||||||||||||||||||||||
t.length !== argLength || | ||||||||||||||||||||||||
t.raw.length !== argLength || | ||||||||||||||||||||||||
!t.every(v => typeof v === 'string') || | ||||||||||||||||||||||||
!t.raw.every(v => typeof v === 'string') | ||||||||||||||||||||||||
) { | ||||||||||||||||||||||||
// Not a template string array shape, so just push it. | ||||||||||||||||||||||||
iargs.push(arg); | ||||||||||||||||||||||||
nextStr === undefined || itmpl.push(nextStr); | ||||||||||||||||||||||||
continue; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Join the current outer template string with the first inner one. | ||||||||||||||||||||||||
itmpl[itmpl.length - 1] += t[0]; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Push the rest of the inner template strings and arguments. | ||||||||||||||||||||||||
itmpl.push(...t.slice(1)); | ||||||||||||||||||||||||
iargs.push(...arg.slice(1)); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if (nextStr !== undefined) { | ||||||||||||||||||||||||
// Join the last inner template string with the next outer one. | ||||||||||||||||||||||||
itmpl[itmpl.length - 1] += nextStr; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return /** @type {const} */ ([makeTemplateStringsArray(itmpl), ...iargs]); | ||||||||||||||||||||||||
}; | ||||||||||||||||||||||||
harden(inlineTemplateArgs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides changing to an ephemeral store, I think we shouldn't burden ourselves with weak collections and their gc costs since we have delimited lifetimes for these host call entries, and don't need to retain them past settlement. Since the replay membrane is keeping the result vow/promises live anyway, there is no reason to assume those might be dropped earlier either.