Skip to content

Commit

Permalink
feat(async-flow): asyncFlow
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed May 19, 2024
1 parent 303a9f2 commit bbb328f
Show file tree
Hide file tree
Showing 35 changed files with 3,761 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test-all-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ jobs:
- name: yarn test (agoric-cli)
if: (success() || failure())
run: cd packages/agoric-cli && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT
- name: yarn test (async-flow)
if: (success() || failure())
run: cd packages/async-flow && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT
- name: yarn test (base-zone)
if: (success() || failure())
run: cd packages/base-zone && yarn ${{ steps.vars.outputs.test }} | $TEST_COLLECT
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 @@ -3,6 +3,7 @@
export default [
"@agoric/access-token",
"@agoric/assert",
"@agoric/async-flow",
"@agoric/base-zone",
"@agoric/benchmark",
"@agoric/boot",
Expand Down
1 change: 1 addition & 0 deletions packages/async-flow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Change Log
40 changes: 40 additions & 0 deletions packages/async-flow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# `@agoric/async-flow`

***Beware that this module may migrate to the endo repository as `@endo/async-flow`.***


Upgrade while suspended at `await` points! Uses membrane to log and replay everything that happened before each upgrade.

In the first incarnation, somewhere, using a ***closed*** async function argument
```js
const wrapperFunc = asyncFlow(
zone,
'funcName`,
async (...) => {... await ...; ...},
);
```
then elsewhere, as often as you'd like
```js
const outcomeVow = wrapperFunc(...);
```

For all these `asyncFlow` calls that happened in the first incarnation, in the first crank of all later incarnations
```js
asyncFlow(
zone,
'funcName`,
async (...) => {... await ...; ...},
);
```
with async functions that reproduce the original's logged behavior. In these later incarnations, you only need to capture the returned `wrapperFunc` if you want to create new activations. Regardless, the old activations continue.

---

> [!IMPORTANT]
> The async function argument should be ***closed***, meaning that it should not use any lexically captured variables other than powerless globals. Any direct access to mutable state or ability to cause effects may introduce bugs, since these effects will happen again under replay outside the control of the asyncFlow isolation and deterministic replay mechanisms.
## Loopholes for purely diagnostic information
>
> We make an explicit exception to the closed-function requirement for `console`, since log messages sent to `console` are only for diagnostic purposes, and `console` as a whole is write-only. We consider the ability to read the console log output to be similar to the ability to view computation through a debugger. Not counting either as "observing effects", the `console` does not cause "observable effects". During replay, such out-of-band console log events may appear again. For the same reason, the async function has no obligation to reproduce previous runs of such out-of-band console logging events, since they are outside the replay mechanisms. Likewise, the guest function has no obligation to reproduce the experience of viewing it through a debugger.
> When comparing arguments sent by the guest function during replay with what the log recorded the guest function to have sent, we are extremely permissive in judging whether a sent error is the "same" as it was on a previous run. We only care that it is an error, and that the value of the `error.name` property is the same string. That string is normally the name of the error "class", such as `TypeError` or `URIError`, and is the only aspect of an error that programs may legitimately use to make a semantically significant decision. Everything else carried by an error, expecially its `error.message`, call-stack information, and subsidiary errors, are only for diagnostic purposes and need not be the same on replay.
Binary file added packages/async-flow/docs/async-flow-states.key
Binary file not shown.
15 changes: 15 additions & 0 deletions packages/async-flow/docs/async-flow-states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Async Flow States

![async flow state diagram](./async-flow-states.png)

A prepared guest async function is like an exoClass (and is internally implemented by an exoClass). It is primarily represented by the host wrapper function that `asyncFlow` returns. Each call on that wrapper function creates an activation of that guest function. A guest activation is like an exoClass instance (and is internally implemented as an instance of the function's internal exoClass). The state diagram shows the lifecycle of a guest function activation

- ***Running***. Invoking the wrapper function creates an activation that is initially in the ***Running*** state. Actions the guest takes in the ***Running*** state, like invoking a host-provided API, cause actual effects and are also recorded for replay. The log records both actions initiated by the guest such as `checkCall`, and actions initiated by the host such as `doFulfill`. But it both cases it logs only host-side objects, since the log needs to survive an upgrade.

- ***Sleeping***. An activation that was ***Running*** just before an upgrade revives into the new incarnation in the ***Sleeping*** state ready to replay from scratch once it awakens. The previous log is intact, but the log's "program counter" is reset to zero. The membrane bijection starts empty since no guest object survives an upgrade. Since an upgrade can only happen between cranks, and therefore between turns, the ***Running*** activation must have been awaiting a vow. When a vow settles, then any ***Sleeping*** activation that might have been awaiting that vow wakes and starts ***Replaying***. An activation can also optionally be configured to be an "eager waker". On revival, a ***Sleeping*** eager waker immediately wakes and starts ***Replaying***. The tradeoff is when to pay the costs of replay.

- ***Replaying***. To start ***Replaying***, the activation first translates the saved activation arguments from host to guest, invokes the guest function, and starts the membrane replaying from its durable log. The replay is finished when the last log entry has been replayed. Once replaying is finished, the activation has caught up and transitions back to ***Running***.

- ***Failed***. If during the ***Replaying*** state the guest activation fails to exactly reproduce its previously logged behavior, it goes into the inactive ***Failed*** state, with a diagnostic explaining how the replay failed, so it can be repaired by another future upgrade. As of the next reincarnation, the failure status is cleared and it starts ***Replaying*** again, hoping not to fail this time. If replay failed because the guest async function did not reproduce its previous behavior, then the upgrade needs to replace the function with one which does. If the replay failed because of a failure of the `asyncFlow` mechanism, whether a bug or merely hitting a case that is not yet implemented, then the upgrade needs to replace the relevant part of `asyncFlow`'s mechanism.

- ***Done***. The guest async function invocation returned a promise for its eventual outcome. Once that promise settles, we assume that the job of the guest activation is done. It then goes into a durably ***Done*** state, dropping all its bookkeeping beyond just remembering the corresponding settled outcome vow, and that it is ***Done***. The replay logs and membrane state of this activation are dropped, to be garbage collected.
Binary file added packages/async-flow/docs/async-flow-states.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/async-flow/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/async-flow.js';
66 changes: 66 additions & 0 deletions packages/async-flow/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "@agoric/async-flow",
"version": "0.1.0",
"description": "Upgrade async functions at await points by replay",
"type": "module",
"repository": "https://github.com/Agoric/agoric-sdk",
"main": "./index.js",
"scripts": {
"build": "exit 0",
"prepack": "tsc --build tsconfig.build.json",
"postpack": "git clean -f '*.d.ts*'",
"test": "ava",
"test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js",
"test:xs": "exit 0",
"lint-fix": "yarn lint:eslint --fix",
"lint": "run-s --continue-on-error lint:*",
"lint:types": "tsc",
"lint:eslint": "eslint ."
},
"exports": {
".": "./index.js"
},
"keywords": [],
"author": "Agoric",
"license": "Apache-2.0",
"dependencies": {
"@agoric/base-zone": "^0.1.0",
"@agoric/store": "^0.9.2",
"@agoric/vow": "^0.1.0",
"@endo/pass-style": "^1.4.0",
"@endo/common": "^1.2.2",
"@endo/errors": "^1.2.2",
"@endo/eventual-send": "^1.2.2",
"@endo/marshal": "^1.5.0",
"@endo/patterns": "^1.4.0",
"@endo/promise-kit": "^1.1.2"
},
"devDependencies": {
"@agoric/internal": "^0.3.2",
"@agoric/swingset-liveslots": "^0.10.2",
"@agoric/zone": "^0.2.2",
"@endo/env-options": "^1.1.4",
"@endo/ses-ava": "^1.2.2",
"ava": "^5.3.0"
},
"publishConfig": {
"access": "public"
},
"engines": {
"node": "^18.12 || ^20.9"
},
"ava": {
"files": [
"test/**/test-*.*",
"test/**/*.test.*"
],
"require": [
"@endo/init/debug.js"
],
"timeout": "20m",
"workerThreads": false
},
"typeCoverage": {
"atLeast": 96.68
}
}
Loading

0 comments on commit bbb328f

Please sign in to comment.