Skip to content

Commit

Permalink
feat(vow): retriable tools
Browse files Browse the repository at this point in the history
  • Loading branch information
mhofman committed Sep 23, 2024
1 parent 49dca57 commit f9075d1
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Generated by [AVA](https://avajs.dev).
chainInfos: {},
connectionInfos: {},
denom: {},
lookupChainInfo_kindHandle: 'Alleged: kind',
lookupChainsAndConnection_kindHandle: 'Alleged: kind',
lookupConnectionInfo_kindHandle: 'Alleged: kind',
},
contract: {
'ChainHub Admin_kindHandle': 'Alleged: kind',
Expand Down Expand Up @@ -72,8 +75,11 @@ Generated by [AVA](https://avajs.dev).
},
},
vows: {
AdminRetriableFlow_kindHandle: 'Alleged: kind',
AdminRetriableFlow_singleton: 'Alleged: AdminRetriableFlow',
PromiseWatcher_kindHandle: 'Alleged: kind',
VowInternalsKit_kindHandle: 'Alleged: kind',
WatchUtils_kindHandle: 'Alleged: kind',
retriableFlowForOutcomeVow: {},
},
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ Generated by [AVA](https://avajs.dev).
chainName: 'agoric',
},
},
lookupChainInfo_kindHandle: 'Alleged: kind',
lookupChainsAndConnection_kindHandle: 'Alleged: kind',
lookupConnectionInfo_kindHandle: 'Alleged: kind',
},
contract: {
'ChainHub Admin_kindHandle': 'Alleged: kind',
Expand Down Expand Up @@ -208,8 +211,11 @@ Generated by [AVA](https://avajs.dev).
},
},
vows: {
AdminRetriableFlow_kindHandle: 'Alleged: kind',
AdminRetriableFlow_singleton: 'Alleged: AdminRetriableFlow',
PromiseWatcher_kindHandle: 'Alleged: kind',
VowInternalsKit_kindHandle: 'Alleged: kind',
WatchUtils_kindHandle: 'Alleged: kind',
retriableFlowForOutcomeVow: {},
},
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ Generated by [AVA](https://avajs.dev).
},
},
denom: {},
lookupChainInfo_kindHandle: 'Alleged: kind',
lookupChainsAndConnection_kindHandle: 'Alleged: kind',
lookupConnectionInfo_kindHandle: 'Alleged: kind',
},
contract: {
orchestration: {
Expand Down Expand Up @@ -144,8 +147,11 @@ Generated by [AVA](https://avajs.dev).
},
},
vows: {
AdminRetriableFlow_kindHandle: 'Alleged: kind',
AdminRetriableFlow_singleton: 'Alleged: AdminRetriableFlow',
PromiseWatcher_kindHandle: 'Alleged: kind',
VowInternalsKit_kindHandle: 'Alleged: kind',
WatchUtils_kindHandle: 'Alleged: kind',
retriableFlowForOutcomeVow: {},
},
}
Binary file not shown.
218 changes: 218 additions & 0 deletions packages/vow/src/retriable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { Fail } from '@endo/errors';
import { M } from '@endo/patterns';
import { PromiseWatcherI } from '@agoric/base-zone';
import { toPassableCap, VowShape } from './vow-utils.js';

/**
* @import {WeakMapStore} from '@agoric/store'
* @import {Zone} from '@agoric/base-zone'
* @import {Vow, VowKit, IsRetryableReason} from './types.js'
* @import {Passable} from '@endo/pass-style'
*/

/**
* @typedef {object} PreparationOptions
* @property {() => VowKit<any>} makeVowKit
* @property {IsRetryableReason} isRetryableReason
*/

/**
* @template {Passable[]} [TArgs=Passable[]]
* @template {any} [TRet=any]
* @typedef {(...args: TArgs) => Promise<TRet>} RetriableFunc
*/

const { defineProperties } = Object;

const RetriableFlowIKit = harden({
flow: M.interface('Flow', {
restart: M.call().returns(),
getOutcome: M.call().returns(VowShape),
}),
resultWatcher: PromiseWatcherI,
});

const AdminRetriableFlowI = M.interface('RetriableFlowAdmin', {
getFlowForOutcomeVow: M.call(VowShape).returns(M.opt(M.remotable('flow'))),
});

/**
* @param {Zone} outerZone
* @param {PreparationOptions} [outerOptions]
*/
export const prepareRetriableTools = (outerZone, outerOptions = {}) => {
const { makeVowKit, isRetryableReason } = outerOptions;

/**
* So we can give out wrapper functions easily and recover flow objects
* for their activations later.
*/
const flowForOutcomeVowKey = outerZone.mapStore(
'retriableFlowForOutcomeVow',
{
keyShape: M.remotable('toPassableCap'),
valueShape: M.remotable('flow'), // isDone === false
},
);

/**
* @param {Zone} zone
* @param {string} tag
* @param {RetriableFunc} retriableFunc
*/
const prepareRetriableFlowKit = (zone, tag, retriableFunc) => {
typeof retriableFunc === 'function' ||
Fail`retriableFunc must be a callable function ${retriableFunc}`;

const internalMakeRetriableFlowKit = zone.exoClassKit(
tag,
RetriableFlowIKit,
activationArgs => {
harden(activationArgs);

return {
activationArgs, // restarting the retriable function uses the original args
outcomeKit: makeVowKit(), // outcome of activation as vow
lastRetryReason: undefined,
runs: 0n,
isDone: false, // persistently done
};
},
{
flow: {
/**
* Calls the retriable function, either for the initial run or when
* the result of the previous run fails with a retriable reason.
*/
restart() {
const { state, facets } = this;
const { activationArgs, isDone } = state;
const { flow, resultWatcher } = facets;

!isDone ||
// separate line so I can set a breakpoint
Fail`Cannot restart a done retriable flow ${flow}`;

const runId = state.runs + 1n;
state.runs = runId;

let resultP;
try {
resultP = Promise.resolve(retriableFunc(...activationArgs));
} catch (err) {
resultP = Promise.resolve(() => Promise.reject(err));
}

outerZone.watchPromise(harden(resultP), resultWatcher, runId);
},
getOutcome() {
const { state } = this;
const { outcomeKit } = state;
return outcomeKit.vow;
},
},
resultWatcher: {
onFulfilled(value, runId) {
const { state } = this;
const { runs, outcomeKit } = state;
if (runId !== runs) return;
!state.isDone ||
Fail`Cannot resolve a done retriable flow ${this.facets.flow}`;
outcomeKit.resolver.resolve(value);
flowForOutcomeVowKey.delete(toPassableCap(outcomeKit.vow));
state.isDone = true;
},
onRejected(reason, runId) {
const { state } = this;
const { runs, outcomeKit } = state;
if (runId !== runs) return;
!state.isDone ||
Fail`Cannot reject a done retriable flow ${this.facets.flow}`;
const retryReason = isRetryableReason(
reason,
state.lastRetryReason,
);
if (retryReason) {
state.lastRetryReason = retryReason;
this.facets.flow.restart();
} else {
outcomeKit.resolver.reject(reason);
flowForOutcomeVowKey.delete(toPassableCap(outcomeKit.vow));
state.isDone = true;
}
},
},
},
);
const makeRetriableFlowKit = activationArgs => {
const retriableKit = internalMakeRetriableFlowKit(activationArgs);
const { flow } = retriableKit;

const vow = flow.getOutcome();
flowForOutcomeVowKey.init(toPassableCap(vow), flow);
flow.restart();
return retriableKit;
};
return harden(makeRetriableFlowKit);
};

/**
* @template {RetriableFunc} F
* @param {Zone} zone
* @param {string} tag
* @param {F} retriableFunc
*/
const retriable = (zone, tag, retriableFunc) => {
const makeRetriableKit = prepareRetriableFlowKit(zone, tag, retriableFunc);
const wrapperFuncName = `${tag}_retriable`;

const wrapperFunc = {
/** @type {(...args: Parameters<F>) => Vow<Awaited<ReturnType<F>>>} */
[wrapperFuncName](...args) {
const { flow } = makeRetriableKit(args);
return flow.getOutcome();
},
}[wrapperFuncName];
defineProperties(wrapperFunc, {
length: { value: retriableFunc.length },
});
return harden(wrapperFunc);
};

const adminRetriableFlow = outerZone.exo(
'AdminRetriableFlow',
AdminRetriableFlowI,
{
getFlowForOutcomeVow(outcomeVow) {
return flowForOutcomeVowKey.get(toPassableCap(outcomeVow));
},
},
);

return harden({
prepareRetriableFlowKit,
adminRetriableFlow,
retriable,
});
};
harden(prepareRetriableTools);

/**
* @typedef {ReturnType<prepareRetriableTools>} RetriableTools
*/

/**
* @typedef {RetriableTools['adminRetriableFlow']} AdminRetriableFlow
*/

/**
* @typedef {ReturnType<RetriableTools['prepareRetriableFlowKit']>} MakeRetriableFlowKit
*/

/**
* @typedef {ReturnType<MakeRetriableFlowKit>} RetriableFlowKit
*/

/**
* @typedef {RetriableFlowKit['flow']} RetriableFlow
*/
22 changes: 5 additions & 17 deletions packages/vow/src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { makeAsVow } from './vow-utils.js';
import { prepareVowKit } from './vow.js';
import { prepareWatchUtils } from './watch-utils.js';
import { prepareWatch } from './watch.js';
import { prepareRetriableTools } from './retriable.js';
import { makeWhen } from './when.js';

/**
Expand Down Expand Up @@ -34,23 +35,10 @@ export const prepareBasicVowTools = (zone, powers = {}) => {
const watchUtils = makeWatchUtils();
const asVow = makeAsVow(makeVowKit);

/**
* TODO FIXME make this real
* Create a function that retries the given function if the underlying
* functions rejects due to upgrade disconnection.
*
* @template {(...args: any[]) => Promise<any>} F
* @param {Zone} fnZone - the zone for the named function
* @param {string} name
* @param {F} fn
* @returns {F extends (...args: infer Args) => Promise<infer R> ? (...args: Args) => Vow<R> : never}
*/
const retriable =
(fnZone, name, fn) =>
// @ts-expect-error cast
(...args) => {
return watch(fn(...args));
};
const { retriable } = prepareRetriableTools(zone, {
makeVowKit,
isRetryableReason,
});

/**
* Vow-tolerant implementation of Promise.all that takes an iterable of vows
Expand Down

0 comments on commit f9075d1

Please sign in to comment.