From c0ead8258a40053d83698a0b9aebc73d923b68b2 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Fri, 7 Jun 2024 09:44:36 +0000 Subject: [PATCH 1/8] test(vow): add test of more vow upgrade scenarios --- .../boot/test/upgrading/upgrade-vats.test.js | 65 +++++++++++++++---- packages/boot/test/upgrading/vat-vow.js | 49 +++++++++++++- 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/packages/boot/test/upgrading/upgrade-vats.test.js b/packages/boot/test/upgrading/upgrade-vats.test.js index 252eb42de81..2d049e1c756 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.js +++ b/packages/boot/test/upgrading/upgrade-vats.test.js @@ -445,6 +445,8 @@ test('upgrade vat-priceAuthority', async t => { matchRef(t, reincarnatedRegistry.adminFacet, registry.adminFacet); }); +const dataOnly = obj => JSON.parse(JSON.stringify(obj)); + test('upgrade vat-vow', async t => { const bundles = { vow: { @@ -464,15 +466,37 @@ test('upgrade vat-vow', async t => { t.log('test incarnation 0'); /** @type {Record} */ const localPromises = { - forever: [], - fulfilled: ['hello'], - rejected: ['goodbye', true], + promiseForever: [], + promiseFulfilled: ['hello'], + promiseRejected: ['goodbye', true], + }; + const localVows = { + vowForever: [], + vowFulfilled: ['hello'], + vowRejected: ['goodbye', true], + vowPostUpgrade: [], + vowPromiseForever: [undefined, false, true], }; await EV(vowRoot).makeLocalPromiseWatchers(localPromises); - t.deepEqual(await EV(vowRoot).getWatcherResults(), { - fulfilled: { status: 'fulfilled', value: 'hello' }, - forever: { status: 'unsettled' }, - rejected: { status: 'rejected', reason: 'goodbye' }, + await EV(vowRoot).makeLocalVowWatchers(localVows); + t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), { + promiseForever: { status: 'unsettled' }, + promiseFulfilled: { status: 'fulfilled', value: 'hello' }, + promiseRejected: { status: 'rejected', reason: 'goodbye' }, + vowForever: { + status: 'unsettled', + resolver: {}, + }, + vowFulfilled: { status: 'fulfilled', value: 'hello' }, + vowRejected: { status: 'rejected', reason: 'goodbye' }, + vowPostUpgrade: { + status: 'unsettled', + resolver: {}, + }, + vowPromiseForever: { + status: 'unsettled', + resolver: {}, + }, }); t.log('restart'); @@ -481,9 +505,29 @@ test('upgrade vat-vow', async t => { t.is(incarnationNumber, 1, 'vat must be reincarnated'); t.log('test incarnation 1'); - t.deepEqual(await EV(vowRoot).getWatcherResults(), { - fulfilled: { status: 'fulfilled', value: 'hello' }, - forever: { + const localVowsUpdates = { + vowPostUpgrade: ['bonjour'], + }; + await EV(vowRoot).resolveVowWatchers(localVowsUpdates); + t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), { + promiseForever: { + status: 'rejected', + reason: { + name: 'vatUpgraded', + upgradeMessage: 'vat upgraded', + incarnationNumber: 0, + }, + }, + promiseFulfilled: { status: 'fulfilled', value: 'hello' }, + promiseRejected: { status: 'rejected', reason: 'goodbye' }, + vowForever: { + status: 'unsettled', + resolver: {}, + }, + vowFulfilled: { status: 'fulfilled', value: 'hello' }, + vowRejected: { status: 'rejected', reason: 'goodbye' }, + vowPostUpgrade: { status: 'fulfilled', value: 'bonjour' }, + vowPromiseForever: { status: 'rejected', reason: { name: 'vatUpgraded', @@ -491,6 +535,5 @@ test('upgrade vat-vow', async t => { incarnationNumber: 0, }, }, - rejected: { status: 'rejected', reason: 'goodbye' }, }); }); diff --git a/packages/boot/test/upgrading/vat-vow.js b/packages/boot/test/upgrading/vat-vow.js index e8cebdb0bee..49fb365b47c 100644 --- a/packages/boot/test/upgrading/vat-vow.js +++ b/packages/boot/test/upgrading/vat-vow.js @@ -4,9 +4,11 @@ import { Far } from '@endo/far'; export const buildRootObject = (_vatPowers, _args, baggage) => { const zone = makeDurableZone(baggage); - const { watch } = prepareVowTools(zone.subZone('VowTools')); + const { watch, makeVowKit } = prepareVowTools(zone.subZone('VowTools')); - /** @type {MapStore>} */ + /** @typedef {{ status: 'unsettled', resolver?: import('@agoric/vow').VowResolver } | PromiseSettledResult} WatcherResult */ + + /** @type {MapStore} */ const nameToResult = zone.mapStore('nameToResult'); const makeWatcher = zone.exoClass('Watcher', undefined, name => ({ name }), { @@ -43,5 +45,48 @@ export const buildRootObject = (_vatPowers, _args, baggage) => { watch(p, makeWatcher(name)); } }, + /** @param {Record} localVows */ + async makeLocalVowWatchers(localVows) { + for (const [name, settlement] of Object.entries(localVows)) { + const { vow, resolver } = makeVowKit(); + nameToResult.init(name, harden({ status: 'unsettled', resolver })); + if (settlement.length) { + let [settlementValue, isRejection] = settlement; + const wrapInPromise = settlement[2]; + if (wrapInPromise) { + if (isRejection) { + settlementValue = Promise.reject(settlementValue); + isRejection = false; + } else if (settlementValue === undefined) { + // Consider an undefined value as no settlement + settlementValue = new Promise(() => {}); + } else { + settlementValue = Promise.resolve(settlementValue); + } + } + if (isRejection) { + resolver.reject(settlementValue); + } else { + resolver.resolve(settlementValue); + } + } + watch(vow, makeWatcher(name)); + } + }, + /** @param {Record} localVows */ + async resolveVowWatchers(localVows) { + for (const [name, settlement] of Object.entries(localVows)) { + const { status, resolver } = nameToResult.get(name); + if (status !== 'unsettled' || !resolver) { + throw Error(`Invalid pending vow for ${name}`); + } + const [settlementValue, isRejection] = settlement; + if (isRejection) { + resolver.reject(settlementValue); + } else { + resolver.resolve(settlementValue); + } + } + }, }); }; From a944c7a079a8f310b42577d317c929222fe725d5 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Fri, 7 Jun 2024 09:45:34 +0000 Subject: [PATCH 2/8] test: switch vow test to run under xs for metering --- packages/SwingSet/tools/bootstrap-relay.js | 9 ++++++--- packages/boot/test/upgrading/upgrade-vats.test.js | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/SwingSet/tools/bootstrap-relay.js b/packages/SwingSet/tools/bootstrap-relay.js index f054f567ae5..a1182e5fbbb 100644 --- a/packages/SwingSet/tools/bootstrap-relay.js +++ b/packages/SwingSet/tools/bootstrap-relay.js @@ -37,10 +37,13 @@ export const buildRootObject = () => { return root; }, - createVat: async ({ name, bundleCapName, vatParameters = {} }) => { + createVat: async ( + { name, bundleCapName, vatParameters = {} }, + options = {}, + ) => { const bcap = await E(vatAdmin).getNamedBundleCap(bundleCapName); - const options = { vatParameters }; - const { adminNode, root } = await E(vatAdmin).createVat(bcap, options); + const vatOptions = { ...options, vatParameters }; + const { adminNode, root } = await E(vatAdmin).createVat(bcap, vatOptions); vatData.set(name, { adminNode, root }); return root; }, diff --git a/packages/boot/test/upgrading/upgrade-vats.test.js b/packages/boot/test/upgrading/upgrade-vats.test.js index 2d049e1c756..b3543b9332f 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.js +++ b/packages/boot/test/upgrading/upgrade-vats.test.js @@ -456,12 +456,15 @@ test('upgrade vat-vow', async t => { const { EV } = await makeScenario(t, { bundles }); - t.log('create initial version'); + t.log('create initial version, metered'); + const vatAdmin = await EV.vat('bootstrap').getVatAdmin(); + const meter = await EV(vatAdmin).createUnlimitedMeter(); const vowVatConfig = { name: 'vow', bundleCapName: 'vow', }; - const vowRoot = await EV.vat('bootstrap').createVat(vowVatConfig); + const vatOptions = { managerType: 'xs-worker', meter }; + const vowRoot = await EV.vat('bootstrap').createVat(vowVatConfig, vatOptions); t.log('test incarnation 0'); /** @type {Record} */ From fb964d8e92e6779cbe1c4cef0ef67c046010ee19 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Fri, 7 Jun 2024 10:17:02 +0000 Subject: [PATCH 3/8] test(vow): add test for resolving vow to external promise --- packages/SwingSet/tools/bootstrap-relay.js | 7 +++++++ packages/boot/test/upgrading/upgrade-vats.test.js | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/SwingSet/tools/bootstrap-relay.js b/packages/SwingSet/tools/bootstrap-relay.js index a1182e5fbbb..6fa802b397e 100644 --- a/packages/SwingSet/tools/bootstrap-relay.js +++ b/packages/SwingSet/tools/bootstrap-relay.js @@ -1,6 +1,7 @@ import { assert } from '@agoric/assert'; import { objectMap } from '@agoric/internal'; import { Far, E } from '@endo/far'; +import { makePromiseKit } from '@endo/promise-kit'; import { buildManualTimer } from './manual-timer.js'; const { Fail, quote: q } = assert; @@ -80,6 +81,12 @@ export const buildRootObject = () => { return remotable; }, + makePromiseKit: () => { + const { promise, ...resolverMethods } = makePromiseKit(); + const resolver = Far('resolver', resolverMethods); + return harden({ promise, resolver }); + }, + /** * Returns a copy of a remotable's logs. * diff --git a/packages/boot/test/upgrading/upgrade-vats.test.js b/packages/boot/test/upgrading/upgrade-vats.test.js index b3543b9332f..209b8d5b3e6 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.js +++ b/packages/boot/test/upgrading/upgrade-vats.test.js @@ -473,11 +473,13 @@ test('upgrade vat-vow', async t => { promiseFulfilled: ['hello'], promiseRejected: ['goodbye', true], }; + const promiseKit = await EV.vat('bootstrap').makePromiseKit(); const localVows = { vowForever: [], vowFulfilled: ['hello'], vowRejected: ['goodbye', true], vowPostUpgrade: [], + vowExternalPromise: [promiseKit.promise], vowPromiseForever: [undefined, false, true], }; await EV(vowRoot).makeLocalPromiseWatchers(localPromises); @@ -496,6 +498,10 @@ test('upgrade vat-vow', async t => { status: 'unsettled', resolver: {}, }, + vowExternalPromise: { + status: 'unsettled', + resolver: {}, + }, vowPromiseForever: { status: 'unsettled', resolver: {}, @@ -512,6 +518,7 @@ test('upgrade vat-vow', async t => { vowPostUpgrade: ['bonjour'], }; await EV(vowRoot).resolveVowWatchers(localVowsUpdates); + await EV(promiseKit.resolver).resolve('ciao'); t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), { promiseForever: { status: 'rejected', @@ -530,6 +537,7 @@ test('upgrade vat-vow', async t => { vowFulfilled: { status: 'fulfilled', value: 'hello' }, vowRejected: { status: 'rejected', reason: 'goodbye' }, vowPostUpgrade: { status: 'fulfilled', value: 'bonjour' }, + vowExternalPromise: { status: 'fulfilled', value: 'ciao' }, vowPromiseForever: { status: 'rejected', reason: { From 9e1e41ef821d2df073c1fc739f909b57f2bf4537 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Fri, 7 Jun 2024 20:45:43 +0000 Subject: [PATCH 4/8] test(vow): add test for vow based infinite vat ping pong --- .../boot/test/upgrading/upgrade-vats.test.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/boot/test/upgrading/upgrade-vats.test.js b/packages/boot/test/upgrading/upgrade-vats.test.js index 209b8d5b3e6..703822991b7 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.js +++ b/packages/boot/test/upgrading/upgrade-vats.test.js @@ -1,6 +1,7 @@ // @ts-check import { test as anyTest } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; +import { makeTagged } from '@endo/marshal'; import { BridgeId } from '@agoric/internal'; import { buildVatController } from '@agoric/swingset-vat'; import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js'; @@ -466,6 +467,15 @@ test('upgrade vat-vow', async t => { const vatOptions = { managerType: 'xs-worker', meter }; const vowRoot = await EV.vat('bootstrap').createVat(vowVatConfig, vatOptions); + const makeFakeVowKit = async () => { + const internalPromiseKit = await EV.vat('bootstrap').makePromiseKit(); + const fakeVowV0 = await EV.vat('bootstrap').makeRemotable('fakeVowV0', { + shorten: internalPromiseKit.promise, + }); + const fakeVow = makeTagged('Vow', harden({ vowV0: fakeVowV0 })); + return harden({ resolver: internalPromiseKit.resolver, vow: fakeVow }); + }; + t.log('test incarnation 0'); /** @type {Record} */ const localPromises = { @@ -474,12 +484,14 @@ test('upgrade vat-vow', async t => { promiseRejected: ['goodbye', true], }; const promiseKit = await EV.vat('bootstrap').makePromiseKit(); + const fakeVowKit = await makeFakeVowKit(); const localVows = { vowForever: [], vowFulfilled: ['hello'], vowRejected: ['goodbye', true], vowPostUpgrade: [], vowExternalPromise: [promiseKit.promise], + vowExternalVow: [fakeVowKit.vow], vowPromiseForever: [undefined, false, true], }; await EV(vowRoot).makeLocalPromiseWatchers(localPromises); @@ -502,6 +514,10 @@ test('upgrade vat-vow', async t => { status: 'unsettled', resolver: {}, }, + vowExternalVow: { + status: 'unsettled', + resolver: {}, + }, vowPromiseForever: { status: 'unsettled', resolver: {}, @@ -519,6 +535,15 @@ test('upgrade vat-vow', async t => { }; await EV(vowRoot).resolveVowWatchers(localVowsUpdates); await EV(promiseKit.resolver).resolve('ciao'); + t.timeout(10_000); + await EV(fakeVowKit.resolver).reject( + harden({ + name: 'vatUpgraded', + upgradeMessage: 'vat upgraded', + incarnationNumber: 0, + }), + ); + t.timeout(600_000); // t.timeout.clear() not yet available in our ava version t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), { promiseForever: { status: 'rejected', @@ -538,6 +563,14 @@ test('upgrade vat-vow', async t => { vowRejected: { status: 'rejected', reason: 'goodbye' }, vowPostUpgrade: { status: 'fulfilled', value: 'bonjour' }, vowExternalPromise: { status: 'fulfilled', value: 'ciao' }, + vowExternalVow: { + status: 'rejected', + reason: { + name: 'vatUpgraded', + upgradeMessage: 'vat upgraded', + incarnationNumber: 0, + }, + }, vowPromiseForever: { status: 'rejected', reason: { From 7858e0c18680de44ff8ea946524c94cf3fbb152f Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Tue, 11 Jun 2024 14:01:17 -0600 Subject: [PATCH 5/8] test(vow): check vow consumers for busy loops or hangs --- .../boot/test/upgrading/upgrade-vats.test.js | 42 ++++++------------- packages/boot/test/upgrading/vat-vow.js | 2 +- 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/packages/boot/test/upgrading/upgrade-vats.test.js b/packages/boot/test/upgrading/upgrade-vats.test.js index 703822991b7..0c0fc4e6c92 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.js +++ b/packages/boot/test/upgrading/upgrade-vats.test.js @@ -536,23 +536,18 @@ test('upgrade vat-vow', async t => { await EV(vowRoot).resolveVowWatchers(localVowsUpdates); await EV(promiseKit.resolver).resolve('ciao'); t.timeout(10_000); - await EV(fakeVowKit.resolver).reject( - harden({ + const upgradeRejection = harden({ + status: 'rejected', + reason: { name: 'vatUpgraded', upgradeMessage: 'vat upgraded', incarnationNumber: 0, - }), - ); + }, + }); + await EV(fakeVowKit.resolver).reject(upgradeRejection.reason); t.timeout(600_000); // t.timeout.clear() not yet available in our ava version t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), { - promiseForever: { - status: 'rejected', - reason: { - name: 'vatUpgraded', - upgradeMessage: 'vat upgraded', - incarnationNumber: 0, - }, - }, + promiseForever: upgradeRejection, promiseFulfilled: { status: 'fulfilled', value: 'hello' }, promiseRejected: { status: 'rejected', reason: 'goodbye' }, vowForever: { @@ -562,22 +557,11 @@ test('upgrade vat-vow', async t => { vowFulfilled: { status: 'fulfilled', value: 'hello' }, vowRejected: { status: 'rejected', reason: 'goodbye' }, vowPostUpgrade: { status: 'fulfilled', value: 'bonjour' }, - vowExternalPromise: { status: 'fulfilled', value: 'ciao' }, - vowExternalVow: { - status: 'rejected', - reason: { - name: 'vatUpgraded', - upgradeMessage: 'vat upgraded', - incarnationNumber: 0, - }, - }, - vowPromiseForever: { - status: 'rejected', - reason: { - name: 'vatUpgraded', - upgradeMessage: 'vat upgraded', - incarnationNumber: 0, - }, - }, + // The 'fulfilled' result below is wishful thinking. Long-lived + // promises are not supported by `watch` at this time. + // vowExternalPromise: { status: 'fulfilled', value: 'ciao' }, + vowExternalPromise: upgradeRejection, + vowExternalVow: upgradeRejection, + vowPromiseForever: upgradeRejection, }); }); diff --git a/packages/boot/test/upgrading/vat-vow.js b/packages/boot/test/upgrading/vat-vow.js index 49fb365b47c..1ba97ac52e3 100644 --- a/packages/boot/test/upgrading/vat-vow.js +++ b/packages/boot/test/upgrading/vat-vow.js @@ -6,7 +6,7 @@ export const buildRootObject = (_vatPowers, _args, baggage) => { const zone = makeDurableZone(baggage); const { watch, makeVowKit } = prepareVowTools(zone.subZone('VowTools')); - /** @typedef {{ status: 'unsettled', resolver?: import('@agoric/vow').VowResolver } | PromiseSettledResult} WatcherResult */ + /** @typedef {({ status: 'unsettled' } | PromiseSettledResult) & { resolver?: import('@agoric/vow').VowResolver }} WatcherResult */ /** @type {MapStore} */ const nameToResult = zone.mapStore('nameToResult'); From 3541b040e40006a86330deecc03c9393466ae013 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Tue, 11 Jun 2024 14:02:31 -0600 Subject: [PATCH 6/8] fix(vow): prevent loops and hangs from watching promises --- packages/vow/src/tools.js | 6 ++++-- packages/vow/src/types.js | 10 ++++++++++ packages/vow/src/watch.js | 25 +++++++++++++++++-------- packages/vow/src/when.js | 31 +++++++++++++++++++++---------- packages/vow/vat.js | 17 +++++++++++------ 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 020298a6056..7351457b6a5 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -5,14 +5,16 @@ import { prepareWatch } from './watch.js'; import { prepareWatchUtils } from './watch-utils.js'; /** @import {Zone} from '@agoric/base-zone' */ +/** @import {IsRetryableReason} from './types.js' */ /** * @param {Zone} zone * @param {object} [powers] - * @param {(reason: any) => boolean} [powers.isRetryableReason] + * @param {IsRetryableReason} [powers.isRetryableReason] */ export const prepareVowTools = (zone, powers = {}) => { - const { isRetryableReason = () => false } = powers; + const { isRetryableReason = /** @type {IsRetryableReason} */ (() => false) } = + powers; const makeVowKit = prepareVowKit(zone); const when = makeWhen(isRetryableReason); const watch = prepareWatch(zone, makeVowKit, isRetryableReason); diff --git a/packages/vow/src/types.js b/packages/vow/src/types.js index bccf77bec71..18a96b78c35 100644 --- a/packages/vow/src/types.js +++ b/packages/vow/src/types.js @@ -10,6 +10,16 @@ export {}; * @import {prepareVowTools} from './tools.js' */ +/** + * @callback IsRetryableReason + * Return truthy if a rejection reason should result in a retry. + * @param {any} reason + * @param {any} priorRetryValue the previous value returned by this function + * when deciding whether to retry the same logical operation + * @returns {any} If falsy, the reason is not retryable. If truthy, the + * priorRetryValue for the next call. + */ + /** * @template T * @typedef {Promise>} PromiseVow Return type of a function that may diff --git a/packages/vow/src/watch.js b/packages/vow/src/watch.js index a7bb6736aad..1f6597cd3dd 100644 --- a/packages/vow/src/watch.js +++ b/packages/vow/src/watch.js @@ -6,7 +6,7 @@ const { apply } = Reflect; /** * @import { PromiseWatcher, Zone } from '@agoric/base-zone'; - * @import { ERef, Vow, VowKit, VowResolver, Watcher } from './types.js'; + * @import { ERef, IsRetryableReason, Vow, VowKit, VowResolver, Watcher } from './types.js'; */ /** @@ -62,7 +62,7 @@ const settle = (resolver, watcher, wcb, value, watcherContext) => { /** * @param {Zone} zone - * @param {(reason: any) => boolean} isRetryableReason + * @param {IsRetryableReason} isRetryableReason * @param {ReturnType} watchNextStep */ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => @@ -80,6 +80,7 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => (resolver, watcher, watcherContext) => { const state = { vow: /** @type {unknown} */ (undefined), + priorRetryValue: /** @type {any} */ (undefined), resolver, watcher, watcherContext: harden(watcherContext), @@ -96,17 +97,25 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => watchNextStep(value, this.self); return; } + this.state.priorRetryValue = undefined; this.state.watcher = undefined; this.state.resolver = undefined; settle(resolver, watcher, 'onFulfilled', value, watcherContext); }, /** @type {Required['onRejected']} */ onRejected(reason) { - const { vow, watcher, watcherContext, resolver } = this.state; - if (vow && isRetryableReason(reason)) { - watchNextStep(vow, this.self); - return; + const { vow, watcher, watcherContext, resolver, priorRetryValue } = + this.state; + if (vow) { + const retryValue = isRetryableReason(reason, priorRetryValue); + if (retryValue) { + // Retry the same specimen. + this.state.priorRetryValue = retryValue; + watchNextStep(vow, this.self); + return; + } } + this.state.priorRetryValue = undefined; this.state.resolver = undefined; this.state.watcher = undefined; settle(resolver, watcher, 'onRejected', reason, watcherContext); @@ -117,12 +126,12 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => /** * @param {Zone} zone * @param {() => VowKit} makeVowKit - * @param {(reason: any) => boolean} [isRetryableReason] + * @param {(reason: any, lastValue: any) => any} [isRetryableReason] */ export const prepareWatch = ( zone, makeVowKit, - isRetryableReason = _reason => false, + isRetryableReason = (_reason, _lastValue) => undefined, ) => { const watchNextStep = makeWatchNextStep(zone); const makePromiseWatcher = preparePromiseWatcher( diff --git a/packages/vow/src/when.js b/packages/vow/src/when.js index aba0ed84925..ef1a6bad3c3 100644 --- a/packages/vow/src/when.js +++ b/packages/vow/src/when.js @@ -1,12 +1,14 @@ // @ts-check import { getVowPayload, basicE } from './vow-utils.js'; -/** @import { Unwrap } from './types.js' */ +/** @import { IsRetryableReason, Unwrap } from './types.js' */ /** - * @param {(reason: any) => boolean} [isRetryableReason] + * @param {IsRetryableReason} [isRetryableReason] */ -export const makeWhen = (isRetryableReason = () => false) => { +export const makeWhen = ( + isRetryableReason = /** @type {IsRetryableReason} */ (() => false), +) => { /** * Shorten `specimenP` until we achieve a final result. * @@ -25,16 +27,25 @@ export const makeWhen = (isRetryableReason = () => false) => { // Ensure we have a presence that won't be disconnected later. let result = await specimenP; let payload = getVowPayload(result); + let priorRetryValue; while (payload) { result = await basicE(payload.vowV0) .shorten() - .catch(e => { - if (isRetryableReason(e)) { - // Shorten the same specimen to try again. - return result; - } - throw e; - }); + .then( + res => { + priorRetryValue = undefined; + return res; + }, + e => { + const nextValue = isRetryableReason(e, priorRetryValue); + if (nextValue) { + // Shorten the same specimen to try again. + priorRetryValue = nextValue; + return result; + } + throw e; + }, + ); // Advance to the next vow. payload = getVowPayload(result); } diff --git a/packages/vow/vat.js b/packages/vow/vat.js index a677ee42eb6..27b7e09a031 100644 --- a/packages/vow/vat.js +++ b/packages/vow/vat.js @@ -4,12 +4,17 @@ import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js'; import { makeHeapZone } from '@agoric/base-zone/heap.js'; import { makeE, prepareVowTools as rawPrepareVowTools } from './src/index.js'; -/** - * Return truthy if a rejection reason should result in a retry. - * @param {any} reason - * @returns {boolean} - */ -const isRetryableReason = reason => isUpgradeDisconnection(reason); +/** @type {import('./src/types.js').IsRetryableReason} */ +const isRetryableReason = (reason, priorRetryValue) => { + if ( + isUpgradeDisconnection(reason) && + (!priorRetryValue || + reason.incarnationNumber > priorRetryValue.incarnationNumber) + ) { + return reason; + } + return undefined; +}; export const defaultPowers = harden({ isRetryableReason, From f3c63933a5df4c0b584d58d7aef0762a5bbcaddb Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Tue, 11 Jun 2024 14:16:54 -0600 Subject: [PATCH 7/8] chore(vat-data): remove the deprecated `@agoric/vat-data/vow.js` Use `@agoric/vow/vat.js` instead. --- packages/vat-data/vow.js | 2 -- packages/vats/src/vat-transfer.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 packages/vat-data/vow.js diff --git a/packages/vat-data/vow.js b/packages/vat-data/vow.js deleted file mode 100644 index 031568326dc..00000000000 --- a/packages/vat-data/vow.js +++ /dev/null @@ -1,2 +0,0 @@ -// Backward-compatibility forwarding to the vow package. -export * from '@agoric/vow/vat.js'; diff --git a/packages/vats/src/vat-transfer.js b/packages/vats/src/vat-transfer.js index b3a78973ae8..fdb85e8c675 100644 --- a/packages/vats/src/vat-transfer.js +++ b/packages/vats/src/vat-transfer.js @@ -3,7 +3,7 @@ import { Far } from '@endo/far'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { provideLazy } from '@agoric/store'; -import { prepareVowTools } from '@agoric/vat-data/vow.js'; +import { prepareVowTools } from '@agoric/vow/vat.js'; import { prepareBridgeTargetModule } from './bridge-target.js'; import { prepareTransferTools } from './transfer.js'; From 945a60cfdadd90716340b5122c4008b56225af7a Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Wed, 12 Jun 2024 12:27:54 -0600 Subject: [PATCH 8/8] fix(vow): allow resolving vow to external promise --- .../boot/test/upgrading/upgrade-vats.test.js | 5 +--- packages/vow/src/vow.js | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/boot/test/upgrading/upgrade-vats.test.js b/packages/boot/test/upgrading/upgrade-vats.test.js index 0c0fc4e6c92..8ffbfb84f87 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.js +++ b/packages/boot/test/upgrading/upgrade-vats.test.js @@ -557,10 +557,7 @@ test('upgrade vat-vow', async t => { vowFulfilled: { status: 'fulfilled', value: 'hello' }, vowRejected: { status: 'rejected', reason: 'goodbye' }, vowPostUpgrade: { status: 'fulfilled', value: 'bonjour' }, - // The 'fulfilled' result below is wishful thinking. Long-lived - // promises are not supported by `watch` at this time. - // vowExternalPromise: { status: 'fulfilled', value: 'ciao' }, - vowExternalPromise: upgradeRejection, + vowExternalPromise: { status: 'fulfilled', value: 'ciao' }, vowExternalVow: upgradeRejection, vowPromiseForever: upgradeRejection, }); diff --git a/packages/vow/src/vow.js b/packages/vow/src/vow.js index d1bd7075efc..742a9c2e737 100644 --- a/packages/vow/src/vow.js +++ b/packages/vow/src/vow.js @@ -69,7 +69,9 @@ export const prepareVowKit = zone => { () => ({ value: undefined, // The stepStatus is null if the promise step hasn't settled yet. - stepStatus: /** @type {null | 'fulfilled' | 'rejected'} */ (null), + stepStatus: /** @type {null | 'pending' | 'fulfilled' | 'rejected'} */ ( + null + ), }), { vowV0: { @@ -84,6 +86,7 @@ export const prepareVowKit = zone => { case 'rejected': throw value; case null: + case 'pending': return provideCurrentKit(this.facets.resolver).promise; default: throw new TypeError(`unexpected stepStatus ${stepStatus}`); @@ -96,10 +99,17 @@ export const prepareVowKit = zone => { */ resolve(value) { const { resolver } = this.facets; - const { promise, resolve } = getPromiseKitForResolution(resolver); + const { stepStatus } = this.state; + const { resolve } = getPromiseKitForResolution(resolver); if (resolve) { resolve(value); - zone.watchPromise(promise, this.facets.watchNextStep); + } + if (stepStatus === null) { + this.state.stepStatus = 'pending'; + zone.watchPromise( + HandledPromise.resolve(value), + this.facets.watchNextStep, + ); } }, /** @@ -107,15 +117,23 @@ export const prepareVowKit = zone => { */ reject(reason) { const { resolver, watchNextStep } = this.facets; + const { stepStatus } = this.state; const { reject } = getPromiseKitForResolution(resolver); if (reject) { reject(reason); + } + if (stepStatus === null) { watchNextStep.onRejected(reason); } }, }, watchNextStep: { onFulfilled(value) { + const { resolver } = this.facets; + const { resolve } = getPromiseKitForResolution(resolver); + if (resolve) { + resolve(value); + } this.state.stepStatus = 'fulfilled'; this.state.value = value; },