From fc1a6b3a26f1b318ff35bde4bd073654c813b14b Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Fri, 21 Jun 2024 09:40:19 -0700 Subject: [PATCH 1/2] feat(vow): retrier --- packages/vow/package.json | 1 + packages/vow/src/retrier.js | 137 ++++++++++++++++++++++++++++++ packages/vow/test/retrier.test.js | 130 ++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 packages/vow/src/retrier.js create mode 100644 packages/vow/test/retrier.test.js diff --git a/packages/vow/package.json b/packages/vow/package.json index 0f9faf2ff38..9986603ead7 100755 --- a/packages/vow/package.json +++ b/packages/vow/package.json @@ -34,6 +34,7 @@ "@agoric/zone": "^0.2.2", "@endo/far": "^1.1.6", "@endo/init": "^1.1.5", + "@endo/ses-ava": "^1.2.6", "ava": "^5.3.0", "tsd": "^0.31.1" }, diff --git a/packages/vow/src/retrier.js b/packages/vow/src/retrier.js new file mode 100644 index 00000000000..ba89e7f9974 --- /dev/null +++ b/packages/vow/src/retrier.js @@ -0,0 +1,137 @@ +import { M } from '@endo/patterns'; +import { PromiseWatcherI } from '@agoric/base-zone'; +import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js'; +import { heapVowE, prepareVowTools } from '../vat.js'; +import { VowShape, toPassableCap } from './vow-utils.js'; + +/** + * @import { Zone } from '@agoric/base-zone' + * @import {VowTools} from './tools.js' + */ + +const RetrierI = M.interface('Retrier', { + retry: M.call().returns(), + getVow: M.call().returns(VowShape), + cancel: M.call(M.error()).returns(), +}); + +const RetrierShape = M.remotable('retrier'); + +const RetrierAdminI = M.interface('RetrierAdmin', { + // modeled on getFlowForOutcomeVow + getRetrierForOutcomeVow: M.call(VowShape).returns(M.opt(RetrierShape)), +}); + +/** + * @param {Zone} zone + * @param {VowTools} [vowTools] + */ +export const prepareRetrierTools = (zone, vowTools = prepareVowTools(zone)) => { + const { makeVowKit, watch } = vowTools; + const retrierForOutcomeVowKey = zone.mapStore('retrierForOutcomeVow', { + keyShape: M.remotable('toPassableCap'), + valueShape: RetrierShape, + }); + + const makeRetrierKit = zone.exoClassKit( + 'Retrier', + { + retrier: RetrierI, + watcher: PromiseWatcherI, + }, + (target, optVerb, args) => { + const { vow, resolver } = makeVowKit(); + return harden({ + target, + optVerb, + args, + vow, + optResolver: resolver, + }); + }, + { + retrier: { + retry() { + const { state, facets } = this; + const { target, optVerb, args, optResolver } = state; + const { watcher } = facets; + + if (optResolver === undefined) { + return; + } + // TODO `heapVowE` is likely too fragile under upgrade. + const p = optVerb + ? heapVowE(target)[optVerb](...args) + : heapVowE(target)(...args); + watch(p, watcher); + }, + getVow() { + const { state } = this; + const { vow } = state; + return vow; + }, + cancel(reason) { + const { state } = this; + if (state.optResolver === undefined) { + return; + } + state.optResolver.resolve(reason); + state.optResolver = undefined; + }, + }, + watcher: { + onFulfilled(value) { + const { state } = this; + + if (state.optResolver === undefined) { + return; + } + state.optResolver.resolve(value); + state.optResolver = undefined; + }, + onRejected(reason) { + const { state, facets } = this; + const { retrier } = facets; + + if (state.optResolver === undefined) { + return; + } + if (isUpgradeDisconnection(reason)) { + // TODO do I need to wait for a new incarnation + // using isRetryableReason instead? + retrier.retry(); + return; + } + state.optResolver.reject(reason); + state.optResolver = undefined; + }, + }, + }, + { + finish({ state, facets }) { + const { vow } = state; + const { retrier } = facets; + retrier.retry(); + retrierForOutcomeVowKey.init(toPassableCap(vow), retrier); + }, + }, + ); + + const retrierAdmin = zone.exo('retrierAdmin', RetrierAdminI, { + getRetrierForOutcomeVow(vow) { + return retrierForOutcomeVowKey.get(toPassableCap(vow)); + }, + }); + + const retry = (target, optVerb, args) => { + const { retrier } = makeRetrierKit(target, optVerb, args); + return retrier.getVow(); + }; + + return harden({ + makeRetrierKit, + retrierAdmin, + retry, + }); +}; +harden(prepareRetrierTools); diff --git a/packages/vow/test/retrier.test.js b/packages/vow/test/retrier.test.js new file mode 100644 index 00000000000..55a1a708bf7 --- /dev/null +++ b/packages/vow/test/retrier.test.js @@ -0,0 +1,130 @@ +import '@agoric/swingset-liveslots/tools/prepare-test-env.js'; +import test from '@endo/ses-ava/prepare-endo.js'; + +import { Fail } from '@endo/errors'; +import { passStyleOf } from '@endo/pass-style'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { makeHeapZone } from '@agoric/zone/heap.js'; +import { makeVirtualZone } from '@agoric/zone/virtual.js'; +import { makeDurableZone } from '@agoric/zone/durable.js'; +import { reincarnate } from '@agoric/swingset-liveslots/tools/setup-vat-data.js'; +import { makeUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js'; + +import { prepareVowTools } from '../vat.js'; +import { prepareRetrierTools } from '../src/retrier.js'; +import { isVow } from '../src/vow-utils.js'; + +/** + * @import {Zone} from '@agoric/base-zone' + */ + +/** @type {ReturnType} */ +let incarnation; + +const annihilate = () => { + incarnation = reincarnate({ relaxDurabilityRules: false }); +}; + +const getBaggage = () => { + return incarnation.fakeVomKit.cm.provideBaggage(); +}; + +const nextLife = () => { + incarnation = reincarnate(incarnation); +}; + +/** + * @param {any} t + * @param {Zone} zone + */ +const retrierPlay1 = async (t, zone) => { + const vowTools = prepareVowTools(zone); + const { retry, retrierAdmin } = prepareRetrierTools(zone, vowTools); + + const makeBob = zone.exoClass('Bob', undefined, count => ({ count }), { + foo(carol) { + const { state } = this; + state.count += 1; + carol.ping(state.count); + if (state.count < 102) { + throw makeUpgradeDisconnection('emulated upgrade1', state.count); + } else { + t.log('postponed at', state.count); + return new Promise(() => {}); // never resolves + } + }, + }); + const bob = makeBob(100); + const carol = zone.exo('carol', undefined, { + ping(count) { + t.log('ping at', count); + }, + }); + const v = zone.makeOnce('v', () => retry(bob, 'foo', [carol])); + t.true(isVow(v)); + t.is(passStyleOf(retrierAdmin.getRetrierForOutcomeVow(v)), 'remotable'); +}; + +/** + * @param {any} t + * @param {Zone} zone + */ +const retrierPlay2 = async (t, zone) => { + const vowTools = prepareVowTools(zone); + const { when } = vowTools; + const { retrierAdmin } = prepareRetrierTools(zone, vowTools); + + zone.exoClass('Bob', undefined, count => ({ count }), { + foo(carol) { + const { state } = this; + t.true(state.count >= 102); + state.count += 1; + carol.ping(state.count); + if (state.count < 104) { + throw makeUpgradeDisconnection('emulated upgrade2', state.count); + } else { + return carol; + } + }, + }); + const carol = zone.exo('carol', undefined, { + ping(count) { + t.log('ping at', count); + }, + }); + const v = zone.makeOnce('v', () => Fail`need v`); + t.true(isVow(v)); + const retrier = retrierAdmin.getRetrierForOutcomeVow(v); + + // Emulate waking up after upgrade + // Should only be needed because of low fidelity of this + // lightweight upgrade testing framework. + // TODO remove once ported to a higher fidelity upgrade testing framework. + // See https://github.com/Agoric/agoric-sdk/issues/9303 + retrier.retry(); + t.is(await when(v), carol); + t.log('carol finally returned'); +}; + +test.serial('test heap retrier', async t => { + const zone = makeHeapZone('heapRoot'); + return retrierPlay1(t, zone); +}); + +test.serial('test virtual retrier', async t => { + annihilate(); + const zone = makeVirtualZone('virtualRoot'); + return retrierPlay1(t, zone); +}); + +test.serial('test durable retrier', async t => { + annihilate(); + const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); + await retrierPlay1(t, zone1); + + await eventLoopIteration(); + + nextLife(); + const zone2 = makeDurableZone(getBaggage(), 'durableRoot'); + await retrierPlay2(t, zone2); +}); From 3dc8fc6b8250cd5d405d0478c274da2a3a08b62d Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 3 Sep 2024 16:36:43 -0700 Subject: [PATCH 2/2] fixup! review suggestions --- packages/vow/src/retrier.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/vow/src/retrier.js b/packages/vow/src/retrier.js index ba89e7f9974..744627e3987 100644 --- a/packages/vow/src/retrier.js +++ b/packages/vow/src/retrier.js @@ -60,9 +60,10 @@ export const prepareRetrierTools = (zone, vowTools = prepareVowTools(zone)) => { return; } // TODO `heapVowE` is likely too fragile under upgrade. - const p = optVerb - ? heapVowE(target)[optVerb](...args) - : heapVowE(target)(...args); + const p = + optVerb === undefined + ? heapVowE(target)(...args) + : heapVowE(target)[optVerb](...args); watch(p, watcher); }, getVow() { @@ -72,11 +73,13 @@ export const prepareRetrierTools = (zone, vowTools = prepareVowTools(zone)) => { }, cancel(reason) { const { state } = this; + const { vow } = state; if (state.optResolver === undefined) { return; } - state.optResolver.resolve(reason); + state.optResolver.reject(reason); state.optResolver = undefined; + retrierForOutcomeVowKey.delete(toPassableCap(vow)); }, }, watcher: {