diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 3f6d870a4309..710317327206 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -250,16 +250,18 @@ export const makeReplayMembrane = ( void when( hVow, - hostFulfillment => { - if (!log.isReplaying() && guestPromiseMap.get(promise) !== 'settled') { + async hostFulfillment => { + await log.promiseReplayDone(); + if (guestPromiseMap.get(promise) !== 'settled') { /** @type {LogEntry} */ const entry = harden(['doFulfill', hVow, hostFulfillment]); log.pushEntry(entry); interpretOne(topDispatch, entry); } }, - hostReason => { - if (!log.isReplaying() && guestPromiseMap.get(promise) !== 'settled') { + async hostReason => { + await log.promiseReplayDone(); + if (guestPromiseMap.get(promise) !== 'settled') { /** @type {LogEntry} */ const entry = harden(['doReject', hVow, hostReason]); log.pushEntry(entry); diff --git a/packages/async-flow/test/test-async-flow.js b/packages/async-flow/test/test-async-flow.js index 736d648ca702..44de63967a58 100644 --- a/packages/async-flow/test/test-async-flow.js +++ b/packages/async-flow/test/test-async-flow.js @@ -7,13 +7,11 @@ import { } from './prepare-test-env-ava.js'; import { Fail } from '@endo/errors'; -// import { E } from '@endo/far'; -// import E from '@agoric/vow/src/E.js'; import { passStyleOf } from '@endo/pass-style'; import { makeCopyMap } from '@endo/patterns'; +import { makePromiseKit } from '@endo/promise-kit'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { isVow } from '@agoric/vow/src/vow-utils.js'; -import { makePromiseKit } from '@endo/promise-kit'; import { prepareVowTools } from '@agoric/vow'; import { prepareVowTools as prepareWatchableVowTools } from '@agoric/vat-data/vow.js'; import { makeHeapZone } from '@agoric/zone/heap.js'; @@ -68,16 +66,17 @@ const testFirstPlay = async (t, zone, vowTools) => { const { vow: v1, resolver: r1 } = zone.makeOnce('v1', () => makeVowKit()); const { vow: v2, resolver: r2 } = zone.makeOnce('v2', () => makeVowKit()); + const { vow: v3, resolver: _r3 } = zone.makeOnce('v3', () => makeVowKit()); const hOrch7 = zone.makeOnce('hOrch7', () => makeOrchestra(7, v2, r2)); // purposely violate rule that guestMethod is closed. const { promise: promiseStep, resolve: resolveStep } = makePromiseKit(); const { guestMethod } = { - async guestMethod(gOrch7, gP) { + async guestMethod(gOrch7, gP, _p3) { t.log(' firstPlay about to await gP'); await gP; - const g2 = gOrch7.vow(); + const p2 = gOrch7.vow(); const prod = gOrch7.scale(3); t.is(prod, 21); @@ -90,16 +89,16 @@ const testFirstPlay = async (t, zone, vowTools) => { t.is(gErr.name, 'TypeError'); resolveStep(true); - t.log(' firstPlay to hang awaiting g2'); + t.log(' firstPlay to hang awaiting p2'); // awaiting a promise that won't be resolved until next incarnation - await g2; + await p2; t.fail('must not reach here in first incarnation'); }, }; const wrapperFunc = asyncFlow(zone, 'AsyncFlow1', guestMethod); - const outcomeV = zone.makeOnce('outcomeV', () => wrapperFunc(hOrch7, v1)); + const outcomeV = zone.makeOnce('outcomeV', () => wrapperFunc(hOrch7, v1, v3)); t.true(isVow(outcomeV)); r1.resolve('x'); @@ -141,13 +140,13 @@ const testBadReplay = async (t, zone, vowTools) => { prepareOrchestra(zone); const { when } = vowTools; const hOrch7 = /** @type {Orchestra} */ ( - zone.makeOnce('hOrch7', () => Fail`hOrch7 expected`) + zone.makeOnce('hOrch7', () => Fail`need hOrch7`) ); // purposely violate rule that guestMethod is closed. const { promise: promiseStep, resolve: resolveStep } = makePromiseKit(); const { guestMethod } = { - async guestMethod(gOrch7, gP) { + async guestMethod(gOrch7, gP, _p3) { t.log(' badReplay about to await gP'); resolveStep(true); await gP; @@ -165,10 +164,10 @@ const testBadReplay = async (t, zone, vowTools) => { asyncFlow(zone, 'AsyncFlow1', guestMethod); const outcomeV = /** @type {Vow} */ ( - zone.makeOnce('outcomeV', () => Fail`outcomeV expected`) + zone.makeOnce('outcomeV', () => Fail`need outcomeV`) ); - // This unblocks `await g2;` but only after the replay failure is fixed in + // This unblocks `await p2;` but only after the replay failure is fixed in // the next incarnation. hOrch7.resolve('y'); // TODO I shouldn't need to do this. @@ -207,16 +206,16 @@ const testGoodReplay = async (t, zone, vowTools) => { prepareOrchestra(zone, 2); // Note change in new behavior const { when } = vowTools; const hOrch7 = /** @type {Orchestra} */ ( - zone.makeOnce('hOrch7', () => Fail`hOrch7 expected`) + zone.makeOnce('hOrch7', () => Fail`need hOrch7`) ); // purposely violate rule that guestMethod is closed. const { promise: promiseStep, resolve: resolveStep } = makePromiseKit(); const { guestMethod } = { - async guestMethod(gOrch7, gP) { + async guestMethod(gOrch7, gP, p3) { t.log(' goodReplay about to await gP'); await gP; - const g2 = gOrch7.vow(); + const p2 = gOrch7.vow(); const prod = gOrch7.scale(3); t.is(prod, 21); @@ -229,13 +228,16 @@ const testGoodReplay = async (t, zone, vowTools) => { t.is(gErr.name, 'TypeError'); resolveStep(true); - t.log(' goodReplay about to await g2'); + t.log(' goodReplay about to await p2'); // awaiting a promise that won't be resolved until this incarnation - await g2; + await p2; t.log(' goodReplay woke up!'); const prod2 = gOrch7.scale(3); // same question. different answer t.is(prod2, 42); + t.log('about to await p3'); + await p3; + t.log('p3 settled'); }, }; @@ -246,20 +248,22 @@ const testGoodReplay = async (t, zone, vowTools) => { asyncFlow(zone, 'AsyncFlow1', guestMethod); const outcomeV = /** @type {Vow} */ ( - zone.makeOnce('outcomeV', () => Fail`outcomeV expected`) + zone.makeOnce('outcomeV', () => Fail`need outcomeV`) ); // TODO I shouldn't need to do this. await adminAsyncFlow.wakeAll(); const v2 = hOrch7.vow(); t.is(await when(v2), 'y'); + await eventLoopIteration(); const flow = adminAsyncFlow.getFlowForOutcomeVow(outcomeV); t.is(passStyleOf(flow), 'remotable'); await promiseStep; - const { vow: v1 } = zone.makeOnce('v1', () => Fail`v1 expected`); + const { vow: v1 } = zone.makeOnce('v1', () => Fail`need v1`); + const { resolver: r3 } = zone.makeOnce('v3', () => Fail`need v3`); const logDump = flow.dump(); t.is(logDump.length, firstLogLen + 3); @@ -282,6 +286,10 @@ const testGoodReplay = async (t, zone, vowTools) => { ['doReturn', firstLogLen + 1, 42], ]); + // @ts-expect-error TS doesn't know it is a resolver + r3.resolve('done'); + await eventLoopIteration(); + t.is(await when(outcomeV), undefined); t.deepEqual(flow.dump(), []); @@ -301,7 +309,7 @@ const testAfterPlay = async (t, zone, vowTools) => { prepareOrchestra(zone); const { guestMethod } = { - async guestMethod(_gOrch7, _gP) { + async guestMethod(_gOrch7, _gP, _p3) { t.fail('Must not replay this'); }, }; @@ -313,7 +321,7 @@ const testAfterPlay = async (t, zone, vowTools) => { asyncFlow(zone, 'AsyncFlow1', guestMethod); const outcomeV = /** @type {Vow} */ ( - zone.makeOnce('outcomeV', () => Fail`outcomeV expected`) + zone.makeOnce('outcomeV', () => Fail`need outcomeV`) ); t.throws(() => adminAsyncFlow.getFlowForOutcomeVow(outcomeV), { diff --git a/packages/async-flow/test/test-log-store.js b/packages/async-flow/test/test-log-store.js index d2c0ed25714e..62d582e1f246 100644 --- a/packages/async-flow/test/test-log-store.js +++ b/packages/async-flow/test/test-log-store.js @@ -60,9 +60,9 @@ const testLogStoreReplay = async (t, zone, _vowTools) => { prepareLogStore(zone); const log = /** @type {LogStore} */ ( - zone.makeOnce('log', () => Fail`log expected`) + zone.makeOnce('log', () => Fail`need log`) ); - const v1 = /** @type {Vow} */ (zone.makeOnce('v1', () => Fail`v1 expected`)); + const v1 = /** @type {Vow} */ (zone.makeOnce('v1', () => Fail`need v1`)); t.is(log.getIndex(), 0); t.is(log.getLength(), 2); diff --git a/packages/async-flow/test/test-replay-membrane-settlement.js b/packages/async-flow/test/test-replay-membrane-settlement.js new file mode 100644 index 000000000000..89d1b83ddf03 --- /dev/null +++ b/packages/async-flow/test/test-replay-membrane-settlement.js @@ -0,0 +1,154 @@ +// eslint-disable-next-line import/order +import { + test, + getBaggage, + annihilate, + nextLife, +} from './prepare-test-env-ava.js'; + +import { Fail } from '@endo/errors'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { prepareVowTools } from '@agoric/vow'; +import { prepareVowTools as prepareWatchableVowTools } from '@agoric/vat-data/vow.js'; +import { makeHeapZone } from '@agoric/zone/heap.js'; +import { makeVirtualZone } from '@agoric/zone/virtual.js'; +import { makeDurableZone } from '@agoric/zone/durable.js'; + +import { prepareLogStore } from '../src/log-store.js'; +import { prepareWeakBijection } from '../src/weak-bijection.js'; +import { makeReplayMembrane } from '../src/replay-membrane.js'; + +const watchWake = _vowish => {}; +const panic = problem => Fail`panic over ${problem}`; + +/** + * @param {Zone} zone + */ +const preparePingee = zone => + zone.exoClass('Pingee', undefined, () => ({}), { + ping() {}, + }); + +/** + * @param {any} t + * @param {Zone} zone + * @param {VowTools} vowTools + */ +const testFirstPlay = async (t, zone, vowTools) => { + const makeLogStore = prepareLogStore(zone); + const makeBijection = prepareWeakBijection(zone); + const makePingee = preparePingee(zone); + const { makeVowKit } = vowTools; + const { vow: v1, resolver: r1 } = zone.makeOnce('v1', () => makeVowKit()); + const { vow: _v2, resolver: _r2 } = zone.makeOnce('v2', () => makeVowKit()); + + const log = zone.makeOnce('log', () => makeLogStore()); + const bij = zone.makeOnce('bij', makeBijection); + + const mem = makeReplayMembrane(log, bij, vowTools, watchWake, panic); + + const p1 = mem.hostToGuest(v1); + t.deepEqual(log.dump(), []); + + const pingee = zone.makeOnce('pingee', () => makePingee()); + const guestPingee = mem.hostToGuest(pingee); + t.deepEqual(log.dump(), []); + + guestPingee.ping(); + t.deepEqual(log.dump(), [ + // keep on separate lines + ['checkCall', pingee, 'ping', [], 0], + ['doReturn', 0, undefined], + ]); + + r1.resolve('x'); + t.is(await p1, 'x'); + + t.deepEqual(log.dump(), [ + ['checkCall', pingee, 'ping', [], 0], + ['doReturn', 0, undefined], + ['doFulfill', v1, 'x'], + ]); +}; + +/** + * @param {any} t + * @param {Zone} zone + * @param {VowTools} vowTools + */ +const testReplay = async (t, zone, vowTools) => { + prepareLogStore(zone); + prepareWeakBijection(zone); + preparePingee(zone); + const { vow: v1 } = zone.makeOnce('v1', () => Fail`need v1`); + const { vow: v2, resolver: r2 } = zone.makeOnce('v2', () => Fail`need v2`); + + const log = /** @type {LogStore} */ ( + zone.makeOnce('log', () => Fail`need log`) + ); + const bij = /** @type {WeakBijection} */ ( + zone.makeOnce('bij', () => Fail`need bij`) + ); + + const pingee = zone.makeOnce('pingee', () => Fail`need pingee`); + + t.deepEqual(log.dump(), [ + ['checkCall', pingee, 'ping', [], 0], + ['doReturn', 0, undefined], + ['doFulfill', v1, 'x'], + ]); + + const mem = makeReplayMembrane(log, bij, vowTools, watchWake, panic); + t.true(log.isReplaying()); + t.is(log.getIndex(), 0); + + const guestPingee = mem.hostToGuest(pingee); + 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); + + guestPingee.ping(); + t.is(await p1, 'x'); + t.is(await p2, 'y'); + t.false(log.isReplaying()); + + t.deepEqual(log.dump(), [ + ['checkCall', pingee, 'ping', [], 0], + ['doReturn', 0, undefined], + ['doFulfill', v1, 'x'], + ['doFulfill', v2, 'y'], + ]); +}; + +await test.serial('test heap replay-membrane settlement', async t => { + const zone = makeHeapZone('heapRoot'); + const vowTools = prepareVowTools(zone); + return testFirstPlay(t, zone, vowTools); +}); + +await test.serial('test virtual replay-membrane settlement', async t => { + annihilate(); + const zone = makeVirtualZone('virtualRoot'); + const vowTools = prepareVowTools(zone); + return testFirstPlay(t, zone, vowTools); +}); + +await test.serial('test durable replay-membrane settlement', async t => { + annihilate(); + + nextLife(); + const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); + const vowTools1 = prepareWatchableVowTools(zone1); + await testFirstPlay(t, zone1, vowTools1); + + nextLife(); + const zone3 = makeDurableZone(getBaggage(), 'durableRoot'); + const vowTools3 = prepareWatchableVowTools(zone3); + return testReplay(t, zone3, vowTools3); +}); diff --git a/packages/async-flow/test/test-replay-membrane.js b/packages/async-flow/test/test-replay-membrane.js index 9c85b5817a81..74ef71d10dc1 100644 --- a/packages/async-flow/test/test-replay-membrane.js +++ b/packages/async-flow/test/test-replay-membrane.js @@ -8,8 +8,6 @@ import { } from './prepare-test-env-ava.js'; import { Fail } from '@endo/errors'; -// import { E } from '@endo/far'; -// import E from '@agoric/vow/src/E.js'; import { isPromise } from '@endo/promise-kit'; import { prepareVowTools } from '@agoric/vow'; import { prepareVowTools as prepareWatchableVowTools } from '@agoric/vat-data/vow.js'; @@ -127,10 +125,10 @@ const testBadReplay = async (t, zone, vowTools) => { prepareOrchestra(zone); const log = /** @type {LogStore} */ ( - zone.makeOnce('log', () => Fail`log expected`) + zone.makeOnce('log', () => Fail`need log`) ); const bij = /** @type {WeakBijection} */ ( - zone.makeOnce('bij', () => Fail`bij expected`) + zone.makeOnce('bij', () => Fail`need bij`) ); const dump = log.dump(); @@ -173,10 +171,10 @@ const testGoodReplay = async (t, zone, vowTools) => { prepareOrchestra(zone, 2); // 2 is new incarnation behavior change const log = /** @type {LogStore} */ ( - zone.makeOnce('log', () => Fail`log expected`) + zone.makeOnce('log', () => Fail`need log`) ); const bij = /** @type {WeakBijection} */ ( - zone.makeOnce('bij', () => Fail`bij expected`) + zone.makeOnce('bij', () => Fail`need bij`) ); const dump = log.dump();