Skip to content

Commit

Permalink
fixup! delay doFulfill logging until replay is done.
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed May 1, 2024
1 parent 5bd8cc6 commit 88d8ec5
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 33 deletions.
10 changes: 6 additions & 4 deletions packages/async-flow/src/replay-membrane.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
50 changes: 29 additions & 21 deletions packages/async-flow/test/test-async-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -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');
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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);

Expand All @@ -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');
},
};

Expand All @@ -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);
Expand All @@ -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(), []);

Expand All @@ -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');
},
};
Expand All @@ -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), {
Expand Down
4 changes: 2 additions & 2 deletions packages/async-flow/test/test-log-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
154 changes: 154 additions & 0 deletions packages/async-flow/test/test-replay-membrane-settlement.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading

0 comments on commit 88d8ec5

Please sign in to comment.