Skip to content

Commit

Permalink
fix(vow): export vat-compatible tools by default (#9932)
Browse files Browse the repository at this point in the history
closes: #9931

## Description

Too many users of `@agoric/vow` have expected that `prepareVowTools` would be compatible with SwingSet, but as a major footgun, they weren't.  Instead of trying to adjust caller expectations, make SwingSet compatibility the default, which can still be overridden if desired.

### Security Considerations

Makes `@agoric/vow` more tolerant, which will help Vows behave correctly in SwingSet vats.

### Scaling Considerations

n/a

### Documentation Considerations

n/a

### Testing Considerations

Used CI to check for regressions.

### Upgrade Considerations

This change of defaults will take effect upon the vat's upgrade, and doesn't change the shape of its baggage.
  • Loading branch information
mergify[bot] authored Aug 21, 2024
2 parents dfc7a48 + 63f0294 commit aedfac6
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 47 deletions.
32 changes: 19 additions & 13 deletions packages/vow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ Here they are: {
}
```

On Agoric, you can use `V` exported from `@agoric/vow/vat.js`, which
converts a chain of promises and vows to a promise for its final
fulfilment, by unwrapping any intermediate vows:
You can use `heapVowE` exported from `@agoric/vow`, which converts a chain of
promises and vows to a promise for its final fulfilment, by unwrapping any
intermediate vows:

```js
import { V as E } from '@agoric/vow/vat.js';
import { heapVowE as E } from '@agoric/vow';
[...]
const a = await E.when(w1);
const b = await E(w2).something(...args);
Expand All @@ -40,12 +40,13 @@ const b = await E(w2).something(...args);

## Vow Producer

On Agoric, use the following to create and resolve a vow:
Use the following to create and resolve a vow:

```js
// CAVEAT: `V` uses internal ephemeral promises, so while it is convenient,
// CAVEAT: `heapVow*` uses internal ephemeral promises, so while it is convenient,
// it cannot be used by upgradable vats. See "Durability" below:
import { V as E, makeVowKit } from '@agoric/vow/vat.js';
import { heapVowE, heapVowTools } from '@agoric/vow';
const { makeVowKit } = heapVowTools;
[...]
const { resolver, vow } = makeVowKit();
// Send vow to a potentially different vat.
Expand All @@ -56,15 +57,15 @@ resolver.resolve('now you know the answer');

## Durability

The `@agoric/vow/vat.js` module allows vows to integrate Agoric's vat upgrade
mechanism. To create vow tools that deal with durable objects:
By default, the `@agoric/vow` module allows vows to integrate with Agoric's vat
upgrade mechanism. To create vow tools that deal with durable objects:

```js
// NOTE: Cannot use `V` as it has non-durable internal state when unwrapping
// vows. Instead, use the default vow-exposing `E` with the `watch`
// operator.
import { E } from '@endo/far';
import { prepareVowTools } from '@agoric/vow/vat.js';
import { prepareVowTools } from '@agoric/vow';
import { makeDurableZone } from '@agoric/zone';

// Only do the following once at the start of a new vat incarnation:
Expand Down Expand Up @@ -94,20 +95,25 @@ final result:
// that may not be side-effect free.
let result = await specimenP;
let vowInternals = getVowInternals(result);
let disconnectionState = undefined;
// Loop until the result is no longer a vow.
while (vowInternals) {
try {
const shortened = await E(internals.vowV0).shorten();
// WARNING: Do not use `shorten()` in your own code. This is an example
// for didactic purposes only.
const shortened = await E(vowInternals.vowV0).shorten();
const nextInternals = getVowInternals(shortened);
// Atomically update the state.
result = shortened;
vowInternals = nextInternals;
} catch (e) {
if (!isDisconnectionReason(e)) {
const nextDisconnectionState = isDisconnectionReason(e, disconnectionState);
if (!nextDisconnectionState) {
// Not a disconnect, so abort.
throw e;
}
// It was a disconnect, so try again with the same state.
// It was a disconnect, so try again with the updated state.
disconnectionState = nextDisconnectionState;
}
}
return result;
Expand Down
9 changes: 8 additions & 1 deletion packages/vow/src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
// @ts-check
export * from './tools.js';

// We default to the vat-compatible version of this package, which is easy to
// reconfigure if not running under SwingSet.
export * from '../vat.js';
export { default as makeE } from './E.js';
export { VowShape, toPassableCap } from './vow-utils.js';

/**
* @typedef {import('./tools.js').VowTools} VowTools
*/

// eslint-disable-next-line import/export
export * from './types.js';

Expand Down
6 changes: 3 additions & 3 deletions packages/vow/src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { makeWhen } from './when.js';
* @param {object} [powers]
* @param {IsRetryableReason} [powers.isRetryableReason]
*/
export const prepareVowTools = (zone, powers = {}) => {
export const prepareBasicVowTools = (zone, powers = {}) => {
const { isRetryableReason = /** @type {IsRetryableReason} */ (() => false) } =
powers;
const makeVowKit = prepareVowKit(zone);
Expand Down Expand Up @@ -72,6 +72,6 @@ export const prepareVowTools = (zone, powers = {}) => {
retriable,
});
};
harden(prepareVowTools);
harden(prepareBasicVowTools);

/** @typedef {ReturnType<typeof prepareVowTools>} VowTools */
/** @typedef {ReturnType<typeof prepareBasicVowTools>} VowTools */
6 changes: 3 additions & 3 deletions packages/vow/test/asVow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import test from 'ava';
import { E } from '@endo/far';
import { makeHeapZone } from '@agoric/base-zone/heap.js';

import { prepareVowTools } from '../src/tools.js';
import { prepareBasicVowTools } from '../src/tools.js';
import { getVowPayload, isVow } from '../src/vow-utils.js';

test('asVow takes a function that throws/returns synchronously and returns a vow', async t => {
const { watch, when, asVow } = prepareVowTools(makeHeapZone());
const { watch, when, asVow } = prepareBasicVowTools(makeHeapZone());

const fnThatThrows = () => {
throw Error('fail');
Expand Down Expand Up @@ -36,7 +36,7 @@ test('asVow takes a function that throws/returns synchronously and returns a vow
});

test('asVow does not resolve a vow to a vow', async t => {
const { watch, when, asVow } = prepareVowTools(makeHeapZone());
const { watch, when, asVow } = prepareBasicVowTools(makeHeapZone());

const testVow = watch(Promise.resolve('payload'));
const testVowAsVow = asVow(() => testVow);
Expand Down
4 changes: 2 additions & 2 deletions packages/vow/test/disconnect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import test from 'ava';

import { makeHeapZone } from '@agoric/base-zone/heap.js';
import { makeTagged } from '@endo/pass-style';
import { prepareVowTools } from '../src/tools.js';
import { prepareBasicVowTools } from '../src/tools.js';

/** @import {Vow} from '../src/types.js' */

test('retry on disconnection', async t => {
const zone = makeHeapZone();
const isRetryableReason = e => e && e.message === 'disconnected';

const { watch, when } = prepareVowTools(zone, {
const { watch, when } = prepareBasicVowTools(zone, {
isRetryableReason,
});
const makeTestVowV0 = zone.exoClass(
Expand Down
30 changes: 15 additions & 15 deletions packages/vow/test/watch-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import test from 'ava';
import { makeHeapZone } from '@agoric/base-zone/heap.js';
import { E, getInterfaceOf } from '@endo/far';

import { prepareVowTools } from '../src/tools.js';
import { prepareBasicVowTools } from '../src/tools.js';

test('allVows waits for a single vow to complete', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

const testPromiseP = Promise.resolve('promise');
const vowA = watch(testPromiseP);
Expand All @@ -20,7 +20,7 @@ test('allVows waits for a single vow to complete', async t => {

test('allVows waits for an array of vows to complete', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

const testPromiseAP = Promise.resolve('promiseA');
const testPromiseBP = Promise.resolve('promiseB');
Expand All @@ -36,7 +36,7 @@ test('allVows waits for an array of vows to complete', async t => {

test('allVows returns vows in order', async t => {
const zone = makeHeapZone();
const { watch, when, allVows, makeVowKit } = prepareVowTools(zone);
const { watch, when, allVows, makeVowKit } = prepareBasicVowTools(zone);
const kit = makeVowKit();

const testPromiseAP = Promise.resolve('promiseA');
Expand All @@ -55,7 +55,7 @@ test('allVows returns vows in order', async t => {

test('allVows rejects upon first rejection', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

const testPromiseAP = Promise.resolve('promiseA');
const testPromiseBP = Promise.reject(Error('rejectedA'));
Expand All @@ -75,7 +75,7 @@ test('allVows rejects upon first rejection', async t => {

test('allVows can accept vows awaiting other vows', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

const testPromiseAP = Promise.resolve('promiseA');
const testPromiseBP = Promise.resolve('promiseB');
Expand All @@ -93,7 +93,7 @@ test('allVows can accept vows awaiting other vows', async t => {

test('allVows - works with just promises', async t => {
const zone = makeHeapZone();
const { when, allVows } = prepareVowTools(zone);
const { when, allVows } = prepareBasicVowTools(zone);

const result = await when(
allVows([Promise.resolve('promiseA'), Promise.resolve('promiseB')]),
Expand All @@ -104,7 +104,7 @@ test('allVows - works with just promises', async t => {

test('allVows - watch promises mixed with vows', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

const testPromiseP = Promise.resolve('vow');
const vowA = watch(testPromiseP);
Expand All @@ -116,7 +116,7 @@ test('allVows - watch promises mixed with vows', async t => {

test('allVows can accept passable data (PureData)', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

const testPromiseP = Promise.resolve('vow');
const vowA = watch(testPromiseP);
Expand All @@ -135,7 +135,7 @@ const prepareAccount = zone =>

test('allVows supports Promise pipelining', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

// makeAccount returns a Promise
const prepareLocalChain = makeAccount => {
Expand Down Expand Up @@ -170,7 +170,7 @@ test('allVows supports Promise pipelining', async t => {

test('allVows does NOT support Vow pipelining', async t => {
const zone = makeHeapZone();
const { watch, when, allVows } = prepareVowTools(zone);
const { watch, when, allVows } = prepareBasicVowTools(zone);

// makeAccount returns a Vow
const prepareLocalChainVowish = makeAccount => {
Expand Down Expand Up @@ -202,7 +202,7 @@ test('allVows does NOT support Vow pipelining', async t => {

test('asPromise converts a vow to a promise', async t => {
const zone = makeHeapZone();
const { watch, asPromise } = prepareVowTools(zone);
const { watch, asPromise } = prepareBasicVowTools(zone);

const testPromiseP = Promise.resolve('test value');
const vow = watch(testPromiseP);
Expand All @@ -213,7 +213,7 @@ test('asPromise converts a vow to a promise', async t => {

test('asPromise handles vow rejection', async t => {
const zone = makeHeapZone();
const { watch, asPromise } = prepareVowTools(zone);
const { watch, asPromise } = prepareBasicVowTools(zone);

const testPromiseP = Promise.reject(new Error('test error'));
const vow = watch(testPromiseP);
Expand All @@ -223,7 +223,7 @@ test('asPromise handles vow rejection', async t => {

test('asPromise accepts and resolves promises', async t => {
const zone = makeHeapZone();
const { asPromise } = prepareVowTools(zone);
const { asPromise } = prepareBasicVowTools(zone);

const p = Promise.resolve('a promise');
const result = await asPromise(p);
Expand All @@ -232,7 +232,7 @@ test('asPromise accepts and resolves promises', async t => {

test('asPromise handles watcher arguments', async t => {
const zone = makeHeapZone();
const { watch, asPromise } = prepareVowTools(zone);
const { watch, asPromise } = prepareBasicVowTools(zone);

const testPromiseP = Promise.resolve('watcher test');
const vow = watch(testPromiseP);
Expand Down
10 changes: 5 additions & 5 deletions packages/vow/test/watch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import test from 'ava';

import { makeHeapZone } from '@agoric/base-zone/heap.js';

import { prepareVowTools } from '../src/tools.js';
import { prepareBasicVowTools } from '../src/tools.js';

/**
* @import {ExecutionContext} from 'ava'
Expand Down Expand Up @@ -59,7 +59,7 @@ const prepareArityCheckWatcher = (zone, t) => {
*/
test('ack watcher - shim', async t => {
const zone = makeHeapZone();
const { watch, when, makeVowKit } = prepareVowTools(zone);
const { watch, when, makeVowKit } = prepareBasicVowTools(zone);
const makeAckWatcher = prepareAckWatcher(zone, t);

const packet = harden({ portId: 'port-1', channelId: 'channel-1' });
Expand Down Expand Up @@ -112,7 +112,7 @@ test('ack watcher - shim', async t => {
*/
test('watcher args arity - shim', async t => {
const zone = makeHeapZone();
const { watch, when, makeVowKit } = prepareVowTools(zone);
const { watch, when, makeVowKit } = prepareBasicVowTools(zone);
const makeArityCheckWatcher = prepareArityCheckWatcher(zone, t);

const testCases = /** @type {const} */ ({
Expand Down Expand Up @@ -173,7 +173,7 @@ test('watcher args arity - shim', async t => {

test('vow self resolution', async t => {
const zone = makeHeapZone();
const { watch, when, makeVowKit } = prepareVowTools(zone);
const { watch, when, makeVowKit } = prepareBasicVowTools(zone);

// A direct self vow resolution
const { vow: vow1, resolver: resolver1 } = makeVowKit();
Expand Down Expand Up @@ -226,7 +226,7 @@ test('vow self resolution', async t => {

test('disconnection of non-vow informs watcher', async t => {
const zone = makeHeapZone();
const { watch, when } = prepareVowTools(zone, {
const { watch, when } = prepareBasicVowTools(zone, {
isRetryableReason: reason => reason === 'disconnected',
});

Expand Down
16 changes: 11 additions & 5 deletions packages/vow/vat.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
// @ts-check
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';

import { prepareBasicVowTools } from './src/tools.js';
import makeE from './src/E.js';

/** @type {import('./src/types.js').IsRetryableReason} */
const isRetryableReason = (reason, priorRetryValue) => {
Expand All @@ -28,13 +30,17 @@ export const defaultPowers = harden({
/**
* Produce SwingSet-compatible vowTools, with an arbitrary Zone type
*
* @type {typeof rawPrepareVowTools}
* @type {typeof prepareBasicVowTools}
*/
export const prepareSwingsetVowTools = (zone, powers = {}) =>
rawPrepareVowTools(zone, { ...defaultPowers, ...powers });
prepareBasicVowTools(zone, { ...defaultPowers, ...powers });
harden(prepareSwingsetVowTools);

/** @deprecated */
export const prepareVowTools = prepareSwingsetVowTools;
/**
* Reexport as prepareVowTools, since that's the thing that people find easiest
* to reach.
*/
export { prepareSwingsetVowTools as prepareVowTools };

/**
* `vowTools` that are not durable, but are useful in non-durable clients that
Expand Down

0 comments on commit aedfac6

Please sign in to comment.