Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vow): a pattern for promises that survive upgrades #8742

Merged
merged 19 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
793f028
feat(whenable): first cut
michaelfig Oct 29, 2023
c65c774
test(whenable): find bugs and improve the implementation
michaelfig Nov 27, 2023
282b5b5
ci(test-all-packages): add `packages/whenable`
michaelfig Nov 27, 2023
663a5fd
fix(whenable): enhance to pass basic tests
michaelfig Dec 4, 2023
996e33d
feat(whenable): use features from more recent Endo
michaelfig Jan 8, 2024
65bb8eb
refactor(whenable): putting on some polish for reviewers
michaelfig Jan 17, 2024
4afad84
chore(internal): add `whenable.js` to preserve layering
michaelfig Jan 17, 2024
d1ac054
chore(whenable): remove hard dependency on `@agoric/internal`
michaelfig Jan 17, 2024
86ee082
fix(whenable): properly chain `watch`ed whenables
michaelfig Jan 18, 2024
7086fe9
build(whenable): update Endo dependencies
michaelfig Jan 19, 2024
04557ca
fix(whenable): better fidelity of shimmed `watchPromise`
michaelfig Jan 20, 2024
8097d94
chore(whenable): `whenable0` -> `whenableV0`
michaelfig Jan 29, 2024
a4c2ae1
docs(whenable): add some
michaelfig Jan 29, 2024
c6bc209
chore(whenable): copy `E.js` from `@endo/eventual-send`
michaelfig Jan 29, 2024
f649b52
feat(whenable): working version of `E`
michaelfig Jan 30, 2024
871306b
chore(whenable): `prepareWhenableModule` -> `prepareWhenableTools`
michaelfig Jan 31, 2024
9dfa861
chore(vat-data): adopt `@agoric/internal/whenable.js`
michaelfig Feb 1, 2024
b5885f7
chore(vow): rename `Whenable` -> `Vow`
michaelfig Feb 8, 2024
4d9371c
fix(vow): persistent resolution, settler->resolver
michaelfig Feb 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test-all-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ jobs:
- name: yarn test (cosmic-proto)
if: (success() || failure())
run: cd packages/cosmic-proto && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT
- name: yarn test (vow)
if: (success() || failure())
run: cd packages/vow && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT
- name: notify on failure
if: failure() && github.event_name != 'pull_request'
uses: ./.github/actions/notify-status
Expand Down
1 change: 1 addition & 0 deletions packages/agoric-cli/src/sdk-package-names.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default [
"@agoric/vat-data",
"@agoric/vats",
"@agoric/vm-config",
"@agoric/vow",
"@agoric/wallet",
"@agoric/wallet-backend",
"@agoric/xsnap",
Expand Down
9 changes: 4 additions & 5 deletions packages/internal/src/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@ export const makeWithQueue = () => {
};

/**
* @template {any[]} T
* @template R
* @param {(...args: T) => Promise<R>} inner
* @template {(...args: any[]) => any} T
* @param {T} inner
*/
return function withQueue(inner) {
/**
* @param {T} args
* @returns {Promise<R>}
* @param {Parameters<T>} args
* @returns {Promise<Awaited<ReturnType<T>>>}
*/
return function queueCall(...args) {
// Curry the arguments into the inner function, and
Expand Down
5 changes: 4 additions & 1 deletion packages/vat-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
"license": "Apache-2.0",
"dependencies": {
"@agoric/assert": "^0.6.0",
"@agoric/base-zone": "^0.1.0",
"@agoric/internal": "^0.3.2",
"@agoric/store": "^0.9.2",
"@agoric/swingset-liveslots": "^0.10.2"
"@agoric/swingset-liveslots": "^0.10.2",
"@agoric/vow": "^0.1.0"
},
"devDependencies": {
"@endo/init": "^1.0.2",
"@endo/far": "^1.0.2",
"@endo/ses-ava": "^1.1.0",
"ava": "^5.3.0",
"tsd": "^0.30.4"
Expand Down
26 changes: 13 additions & 13 deletions packages/vat-data/src/vat-data-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ if ('VatData' in globalThis) {
// XXX this module has been known to get imported (transitively) in cases that
// never use it so we make a version that will satisfy module resolution but
// fail at runtime.
const unvailable = () => Fail`VatData unavailable`;
const unavailable = () => Fail`VatData unavailable`;
VatDataGlobal = {
defineKind: unvailable,
defineKindMulti: unvailable,
defineDurableKind: unvailable,
defineDurableKindMulti: unvailable,
makeKindHandle: unvailable,
providePromiseWatcher: unvailable,
watchPromise: unvailable,
makeScalarBigMapStore: unvailable,
makeScalarBigWeakMapStore: unvailable,
makeScalarBigSetStore: unvailable,
makeScalarBigWeakSetStore: unvailable,
canBeDurable: unvailable,
defineKind: unavailable,
defineKindMulti: unavailable,
defineDurableKind: unavailable,
defineDurableKindMulti: unavailable,
makeKindHandle: unavailable,
providePromiseWatcher: unavailable,
watchPromise: unavailable,
makeScalarBigMapStore: unavailable,
makeScalarBigWeakMapStore: unavailable,
makeScalarBigSetStore: unavailable,
makeScalarBigWeakSetStore: unavailable,
canBeDurable: unavailable,
};
}

Expand Down
36 changes: 36 additions & 0 deletions packages/vat-data/test/test-vow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @ts-check
import test from 'ava';
import { E, Far } from '@endo/far';

import { V, makeVowKit } from '../vow.js';

test('heap messages', async t => {
const greeter = Far('Greeter', {
hello: /** @param {string} name */ name => `Hello, ${name}!`,
});

/** @type {ReturnType<typeof makeVowKit<typeof greeter>>} */
const { vow, resolver } = makeVowKit();
const retP = V(vow).hello('World');
resolver.resolve(greeter);

// Happy path: WE(vow)[method](...args) calls the method.
t.is(await retP, 'Hello, World!');

// Sad path: E(vow)[method](...args) rejects.
await t.throwsAsync(
// @ts-expect-error hello is not accessible via basicE
() => E(vow).hello('World'),
{
message: /target has no method "hello"/,
},
);

// Happy path: await WE.when unwraps the vow.
t.is(await V.when(vow), greeter);

// Sad path: await by itself gives the raw vow.
const w = await vow;
t.not(w, greeter);
t.truthy(w.payload);
});
47 changes: 47 additions & 0 deletions packages/vat-data/vow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* global globalThis */
// @ts-check
import { makeE, prepareVowTools as rawPrepareVowTools } from '@agoric/vow';
import { makeHeapZone } from '@agoric/base-zone/heap.js';
import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js';

/** @type {any} */
const vatData = globalThis.VatData;

/**
* Manually-extracted watchPromise so we don't accidentally get the 'unavailable'
* version. If it is `undefined`, `@agoric/vow` will shim it.
* @type {undefined | ((
* p: Promise<any>,
* watcher: import('@agoric/vow/src/watch-promise.js').PromiseWatcher,
* ...args: unknown[]
* ) => void)}
*/
const watchPromise = vatData && vatData.watchPromise;

/**
* Return truthy if a rejection reason should result in a retry.
* @param {any} reason
* @returns {boolean}
*/
const rejectionMeansRetry = reason => isUpgradeDisconnection(reason);

export const defaultPowers = harden({
rejectionMeansRetry,
watchPromise,
});

/**
* @type {typeof rawPrepareVowTools}
*/
export const prepareVowTools = (zone, powers = {}) =>
rawPrepareVowTools(zone, { ...defaultPowers, ...powers });

export const { watch, when, makeVowKit } = prepareVowTools(makeHeapZone());

/**
* An vow-shortening E. CAVEAT: This produces long-lived ephemeral
* promises that encapsulate the shortening behaviour, and so provides no way
* for `watch` to durably shorten. Use the standard `import('@endo/far').E` if
* you need to `watch` its resulting promises.
*/
export const V = makeE(globalThis.HandledPromise, { unwrap: when });
Empty file added packages/vow/CHANGELOG.md
Empty file.
78 changes: 78 additions & 0 deletions packages/vow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Vows

Native promises are not compatible with Agoric's durable stores, which means that on the Agoric platform, such promises disconnect their clients when their creator vat is upgraded. Vows are objects that represent promises that can be stored durably, this package also provides a `when` operator to allow clients to tolerate upgrades of vow-hosting vats, as well as a `watch` operator to subscribe to a vow in a way that survives upgrades of both the creator and subscribing client vats.

## Vow Consumer

If your vat is a consumer of promises that are unexpectedly fulfilling to a Vow (something like):

```js
import { E } from '@endo/far';

const a = await w1;
const b = await E(w2).something(...args);
console.log('Here they are:', { a, b });
```

Produces output like:
```console
Here they are: {
a: Object [Vow] {
payload: { vowV0: Object [Alleged: VowInternalsKit vowV0] {} }
},
b: Object [Vow] {
payload: { vowV0: Object [Alleged: VowInternalsKit vowV0] {} }
}
}
```

On Agoric, you can use `V` exported from `@agoric/vat-data/vow.js`, 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/vat-data/vow.js';
[...]
const a = await E.when(w1);
const b = await E(w2).something(...args);
// Produces the expected results.
```

## Vow Producer

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

```js
// CAVEAT: `V` 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/vat-data/vow.js';
[...]
const { resolver, vow } = makeVowKit();
// Send vow to a potentially different vat.
E(outsideReference).performSomeMethod(vow);
// some time later...
resolver.resolve('now you know the answer');
```

## Durability

The vow package supports Zones, which are used to integrate Agoric's vat
upgrade mechanism and `watchPromise`. 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/vat-data/vow.js';
import { makeDurableZone } from '@agoric/zone';

// Only do the following once at the start of a new vat incarnation:
const zone = makeDurableZone(baggage);
const vowZone = zone.subZone('VowTools');
const { watch, makeVowKit } = prepareVowTools(vowZone);

// Now the functions have been bound to the durable baggage.
// Vows and resolvers you create can be saved in durable stores.
```
52 changes: 52 additions & 0 deletions packages/vow/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@agoric/vow",
"version": "0.1.0",
"description": "Remote (shortening and disconnection-tolerant) Promise-likes",
"type": "module",
"main": "src/index.js",
"engines": {
"node": ">=14.15.0"
},
"scripts": {
"build": "exit 0",
"prepack": "tsc --build tsconfig.build.json",
"postpack": "git clean -f '*.d.ts*'",
"test": "ava",
"test:nyc": "exit 0",
"test:xs": "exit 0",
"lint-fix": "yarn lint:eslint --fix",
"lint": "run-s --continue-on-error lint:*",
"lint:eslint": "eslint .",
"lint:types": "tsc"
},
"dependencies": {
"@agoric/base-zone": "^0.1.0",
"@endo/env-options": "^1.1.0",
"@endo/eventual-send": "^1.1.0",
"@endo/pass-style": "^1.1.0",
"@endo/patterns": "^1.1.0",
"@endo/promise-kit": "^1.0.2"
},
"devDependencies": {
"@agoric/internal": "^0.3.2",
"@endo/far": "^1.0.2",
"@endo/init": "^1.0.2",
"ava": "^5.3.0"
},
"ava": {
"require": [
"@endo/init/debug.js"
]
},
"author": "Agoric",
"license": "Apache-2.0",
"files": [
"src"
],
"publishConfig": {
"access": "public"
},
"typeCoverage": {
"atLeast": 92.26
}
}
Loading
Loading