diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 16e19f6a749..bd9da086554 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -10,9 +10,10 @@ import { makeConvertKit } from './convert.js'; import { makeEquate } from './equate.js'; /** - * @import {PromiseKit} from '@endo/promise-kit' - * @import {Passable, PassableCap, CopyTagged} from '@endo/pass-style' - * @import {Vow, VowTools, VowKit} from '@agoric/vow' + * @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 {LogStore} from '../src/log-store.js'; * @import {Bijection} from '../src/bijection.js'; * @import {Host, HostVow, LogEntry, Outcome} from '../src/types.js'; @@ -28,12 +29,29 @@ const { fromEntries, defineProperties, assign } = Object; * @param {(vowish: Promise | Vow) => void} arg.watchWake * @param {(problem: Error) => never} arg.panic */ -export const makeReplayMembrane = ({ +export const makeReplayMembrane = arg => { + const noDunderArg = /** @type {typeof arg} */ ( + Object.fromEntries(Object.entries(arg).filter(([k]) => !k.startsWith('__'))) + ); + return makeReplayMembraneForTesting(noDunderArg); +}; + +/** + * @param {object} arg + * @param {LogStore} arg.log + * @param {Bijection} arg.bijection + * @param {VowTools} arg.vowTools + * @param {(vowish: Promise | Vow) => void} arg.watchWake + * @param {(problem: Error) => never} arg.panic + * @param {boolean} [arg.__eventualSendForTesting] CAVEAT: Only for async-flow tests + */ +export const makeReplayMembraneForTesting = ({ log, bijection, vowTools, watchWake, panic, + __eventualSendForTesting, }) => { const { when, makeVowKit } = vowTools; @@ -218,30 +236,22 @@ export const makeReplayMembrane = ({ // //////////////// Eventual Send //////////////////////////////////////////// /** - * @param {PassableCap} hostTarget + * @param {RemotableBrand} hostTarget * @param {string | undefined} optVerb * @param {Passable[]} hostArgs */ const performSendOnly = (hostTarget, optVerb, hostArgs) => { try { - optVerb - ? heapVowE.sendOnly(hostTarget)[optVerb](...hostArgs) - : // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore once we changed this from E to heapVowE, - // typescript started complaining that heapVowE(hostTarget) - // is not callable. I'm not sure if this is a just a typing bug - // in heapVowE or also reflects a runtime deficiency. But this - // case it not used yet anyway. We disable it - // with at-ts-ignore rather than at-ts-expect-error because - // the dependency-graph tests complains that the latter is unused. - heapVowE.sendOnly(hostTarget)(...hostArgs); + optVerb === undefined + ? heapVowE.sendOnly(hostTarget)(...hostArgs) + : heapVowE.sendOnly(hostTarget)[optVerb](...hostArgs); } catch (hostProblem) { - throw Panic`internal: eventual sendOnly synchrously failed ${hostProblem}`; + throw Panic`internal: eventual sendOnly synchronously failed ${hostProblem}`; } }; /** - * @param {PassableCap} hostTarget + * @param {RemotableBrand} hostTarget * @param {string | undefined} optVerb * @param {Passable[]} hostArgs * @param {number} callIndex @@ -259,20 +269,13 @@ export const makeReplayMembrane = ({ ) => { const { vow, resolver } = hostResultKit; try { - const hostPromise = optVerb - ? heapVowE(hostTarget)[optVerb](...hostArgs) - : // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore once we changed this from E to heapVowE, - // typescript started complaining that heapVowE(hostTarget) - // is not callable. I'm not sure if this is a just a typing bug - // in heapVowE or also reflects a runtime deficiency. But this - // case it not used yet anyway. We disable it - // with at-ts-ignore rather than at-ts-expect-error because - // the dependency-graph tests complains that the latter is unused. - heapVowE(hostTarget)(...hostArgs); + const hostPromise = + optVerb === undefined + ? heapVowE(hostTarget)(...hostArgs) + : heapVowE(hostTarget)[optVerb](...hostArgs); resolver.resolve(hostPromise); // TODO does this always work? } catch (hostProblem) { - throw Panic`internal: eventual send synchrously failed ${hostProblem}`; + throw Panic`internal: eventual send synchronously failed ${hostProblem}`; } try { const entry = harden(['doReturn', callIndex, vow]); @@ -293,6 +296,10 @@ export const makeReplayMembrane = ({ const guestHandler = harden({ applyMethodSendOnly(guestTarget, optVerb, guestArgs) { + __eventualSendForTesting || + Panic`guest eventual applyMethodSendOnly not yet supported: ${guestTarget}.${b(optVerb)}`; + // The following code is only intended to be used by tests. + // TODO: properly support applyMethodSendOnly const callIndex = log.getIndex(); if (stopped || !bijection.hasGuest(guestTarget)) { Fail`Sent from a previous run: ${guestTarget}`; @@ -331,6 +338,10 @@ export const makeReplayMembrane = ({ } }, applyMethod(guestTarget, optVerb, guestArgs, guestReturnedP) { + __eventualSendForTesting || + Panic`guest eventual applyMethod not yet supported: ${guestTarget}.${b(optVerb)} -> ${b(guestReturnedP)}`; + // The following code is only intended to be used by tests. + // TODO: properly support applyMethod const callIndex = log.getIndex(); if (stopped || !bijection.hasGuest(guestTarget)) { Fail`Sent from a previous run: ${guestTarget}`; @@ -398,6 +409,8 @@ export const makeReplayMembrane = ({ } }, applyFunctionSendOnly(guestTarget, guestArgs) { + __eventualSendForTesting || + Panic`guest eventual applyFunctionSendOnly not yet supported: ${guestTarget}`; return guestHandler.applyMethodSendOnly( guestTarget, undefined, @@ -405,6 +418,8 @@ export const makeReplayMembrane = ({ ); }, applyFunction(guestTarget, guestArgs, guestReturnedP) { + __eventualSendForTesting || + Panic`guest eventual applyFunction not yet supported: ${guestTarget} -> ${b(guestReturnedP)}`; return guestHandler.applyMethod( guestTarget, undefined, @@ -413,9 +428,11 @@ export const makeReplayMembrane = ({ ); }, getSendOnly(guestTarget, prop) { + // TODO: support getSendOnly throw Panic`guest eventual getSendOnly not yet supported: ${guestTarget}.${b(prop)}`; }, get(guestTarget, prop, guestReturnedP) { + // TODO: support get throw Panic`guest eventual get not yet supported: ${guestTarget}.${b(prop)} -> ${b(guestReturnedP)}`; }, }); diff --git a/packages/async-flow/test/replay-membrane-eventual.test.js b/packages/async-flow/test/replay-membrane-eventual.test.js index 460b8964348..6e37a2d6bab 100644 --- a/packages/async-flow/test/replay-membrane-eventual.test.js +++ b/packages/async-flow/test/replay-membrane-eventual.test.js @@ -17,10 +17,9 @@ import { makeDurableZone } from '@agoric/zone/durable.js'; import { prepareLogStore } from '../src/log-store.js'; import { prepareBijection } from '../src/bijection.js'; -import { makeReplayMembrane } from '../src/replay-membrane.js'; +import { makeReplayMembraneForTesting } from '../src/replay-membrane.js'; /** - * @import {PromiseKit} from '@endo/promise-kit' * @import {Zone} from '@agoric/base-zone' * @import {LogStore} from '../src/log-store.js'; * @import {Bijection} from '../src/bijection.js'; @@ -41,11 +40,19 @@ const preparePingee = zone => * @typedef {ReturnType>} Pingee */ +const testMode = /** @type {const} */ ({ + normal: 'normal', + noEventualSend: 'noEventualSend', + retry: 'retry', +}); + /** * @param {any} t * @param {Zone} zone + * @param {testMode[keyof testMode]} [mode] */ -const testFirstPlay = async (t, zone) => { +const testFirstPlay = async (t, zone, mode = testMode.normal) => { + t.log('testFirstPlay', mode); const vowTools = prepareVowTools(zone); const { makeVowKit } = vowTools; const makeLogStore = prepareLogStore(zone); @@ -57,22 +64,33 @@ const testFirstPlay = async (t, zone) => { const log = zone.makeOnce('log', () => makeLogStore()); const bijection = zone.makeOnce('bij', makeBijection); - const mem = makeReplayMembrane({ + const mem = makeReplayMembraneForTesting({ log, bijection, vowTools, watchWake, panic, + __eventualSendForTesting: mode !== testMode.noEventualSend, }); - const p1 = mem.hostToGuest(v1); - t.deepEqual(log.dump(), []); - /** @type {Pingee} */ const pingee = zone.makeOnce('pingee', () => makePingee()); + const beforeSend = [ + ['checkCall', pingee, 'ping', ['call'], 0], + ['doReturn', 0, undefined], + ]; + + const initialDump = []; + if (mode === testMode.retry) { + initialDump.push(...beforeSend); + } + + const p1 = mem.hostToGuest(v1); + t.deepEqual(log.dump(), initialDump); + /** @type {Pingee} */ const guestPingee = mem.hostToGuest(pingee); - t.deepEqual(log.dump(), []); + t.deepEqual(log.dump(), initialDump); const p = E(guestPingee).ping('send'); const pOnly = E.sendOnly(guestPingee).ping('sendOnly'); @@ -80,17 +98,29 @@ const testFirstPlay = async (t, zone) => { guestPingee.ping('call'); + /** @type {any[][]} */ + const afterPingDump = [...beforeSend]; + if (mode === testMode.noEventualSend) { + await t.throwsAsync(p, { + message: + /panic over "\[Error: guest eventual applyMethod not yet supported:/, + }); + const dump = log.dump(); + t.deepEqual(dump, afterPingDump); + return p; + } + t.is(await p, undefined); const dump = log.dump(); const v3 = dump[3][2]; - t.deepEqual(dump, [ - ['checkCall', pingee, 'ping', ['call'], 0], - ['doReturn', 0, undefined], + const afterSend = [ ['checkSend', pingee, 'ping', ['send'], 2], ['doReturn', 2, v3], ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], ['doFulfill', v3, undefined], - ]); + ]; + afterPingDump.push(...afterSend); + t.deepEqual(dump, afterPingDump); r1.resolve('x'); t.is(await p1, 'x'); @@ -109,8 +139,10 @@ const testFirstPlay = async (t, zone) => { /** * @param {any} t * @param {Zone} zone + * @param {testMode[keyof testMode]} [mode] */ -const testReplay = async (t, zone) => { +const testReplay = async (t, zone, mode = testMode.normal) => { + t.log('testReplay', mode); const vowTools = prepareVowTools(zone); prepareLogStore(zone); prepareBijection(zone); @@ -129,7 +161,7 @@ const testReplay = async (t, zone) => { const dump = log.dump(); const v3 = dump[3][2]; - t.deepEqual(dump, [ + const beforeY = [ ['checkCall', pingee, 'ping', ['call'], 0], ['doReturn', 0, undefined], ['checkSend', pingee, 'ping', ['send'], 2], @@ -137,14 +169,18 @@ const testReplay = async (t, zone) => { ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], ['doFulfill', v3, undefined], ['doFulfill', v1, 'x'], - ]); + ]; + const afterY = [['doFulfill', v2, 'y']]; + const initialDump = beforeY; + t.deepEqual(dump, initialDump); - const mem = makeReplayMembrane({ + const mem = makeReplayMembraneForTesting({ log, bijection, vowTools, watchWake, panic, + __eventualSendForTesting: mode !== testMode.noEventualSend, }); t.true(log.isReplaying()); t.is(log.getIndex(), 0); @@ -153,23 +189,15 @@ const testReplay = async (t, zone) => { const p2 = mem.hostToGuest(v2); // @ts-expect-error TS doesn't know that r2 is a resolver r2.resolve('y'); - await eventLoopIteration(); const p1 = mem.hostToGuest(v1); mem.wake(); t.true(log.isReplaying()); t.is(log.getIndex(), 0); - t.deepEqual(log.dump(), [ - ['checkCall', pingee, 'ping', ['call'], 0], - ['doReturn', 0, undefined], - ['checkSend', pingee, 'ping', ['send'], 2], - ['doReturn', 2, v3], - ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], - ['doFulfill', v3, undefined], - ['doFulfill', v1, 'x'], - ]); + t.deepEqual(log.dump(), initialDump); + + const pingSend = E(guestPingee).ping('send'); - E(guestPingee).ping('send'); // TODO Once https://github.com/endojs/endo/issues/2336 is fixed, // the following `void` should not be needed. But strangely, TS isn't // telling me a `void` is needed above, which is also incorrect. @@ -177,20 +205,19 @@ const testReplay = async (t, zone) => { guestPingee.ping('call'); - t.is(await p1, 'x'); - t.is(await p2, 'y'); - t.false(log.isReplaying()); - - t.deepEqual(log.dump(), [ - ['checkCall', pingee, 'ping', ['call'], 0], - ['doReturn', 0, undefined], - ['checkSend', pingee, 'ping', ['send'], 2], - ['doReturn', 2, v3], - ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], - ['doFulfill', v3, undefined], - ['doFulfill', v1, 'x'], - ['doFulfill', v2, 'y'], - ]); + let finalDump; + if (mode === testMode.noEventualSend) { + t.true(log.isReplaying()); + finalDump = beforeY; + } else { + t.is(await p1, 'x'); + t.is(await p2, 'y'); + t.false(log.isReplaying()); + finalDump = [...beforeY, ...afterY]; + } + + t.deepEqual(log.dump(), finalDump); + return pingSend; }; test.serial('test heap replay-membrane settlement', async t => { @@ -215,3 +242,32 @@ test.serial('test durable replay-membrane settlement', async t => { const zone3 = makeDurableZone(getBaggage(), 'durableRoot'); return testReplay(t, zone3); }); + +test.serial('test durable toggle eventual send', async t => { + annihilate(); + + nextLife(); + const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); + await t.throwsAsync(() => testFirstPlay(t, zone1, testMode.noEventualSend), { + message: + /^panic over "\[Error: guest eventual applyMethod not yet supported:/, + }); + + await eventLoopIteration(); + nextLife(); + const zone1a = makeDurableZone(getBaggage(), 'durableRoot'); + await testFirstPlay(t, zone1a, testMode.retry); + + await eventLoopIteration(); + nextLife(); + const zone2 = makeDurableZone(getBaggage(), 'durableRoot'); + await t.throwsAsync(() => testReplay(t, zone2, testMode.noEventualSend), { + message: + /^panic over "\[Error: guest eventual applyMethod not yet supported:/, + }); + + await eventLoopIteration(); + nextLife(); + const zone2a = makeDurableZone(getBaggage(), 'durableRoot'); + await testReplay(t, zone2a, testMode.retry); +});