-
Notifications
You must be signed in to change notification settings - Fork 220
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(notifier): subscribeEach/subscribeLatest iterators retry when broken by vat upgrade #7401
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
e96a0ee
feat(notifier): subscribeLatest iterators retry when broken by vat up…
gibson042 ef7fc68
refactor: Move DisconnectionObject generation/testing into "internal"
gibson042 a6b275a
chore(notifier): Annotate subscriber rejections that follow disconnec…
gibson042 bdbbe7d
chore(notifier): Tolerate failure of an opportunistic assert.note()
gibson042 229d7b2
feat(notifier): Opportunistic eachIterator recovery from upgrade disc…
gibson042 9e84193
style(notifier): Rename some bindings to clarify their use
gibson042 10debb3
test(notifier): Reduce nesting for better actual vs. expected diffs
gibson042 19aa90c
chore(notifier): Include all relevant promises in automatic rejection…
gibson042 092e7f3
test(inter-protocol): Increase await-sensitive tolerance in test-stak…
gibson042 61b85e1
test(inter-protocol): Refactor approxEqual into a scale-sensitive ass…
gibson042 e6d65d0
chore(notifier): Clarify makeEachIterator rejection suppression
gibson042 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// @ts-check | ||
// @jessie-check | ||
import { isObject } from '@endo/marshal'; | ||
|
||
/** | ||
* @typedef {{ name: string, upgradeMessage: string, incarnationNumber: number }} DisconnectionObject | ||
*/ | ||
|
||
/** | ||
* Makes an Error-like object for use as the rejection value of promises | ||
* abandoned by upgrade. | ||
* | ||
* @param {string} upgradeMessage | ||
* @param {number} toIncarnationNumber | ||
* @returns {DisconnectionObject} | ||
*/ | ||
export const makeUpgradeDisconnection = (upgradeMessage, toIncarnationNumber) => | ||
harden({ | ||
name: 'vatUpgraded', | ||
upgradeMessage, | ||
incarnationNumber: toIncarnationNumber, | ||
}); | ||
harden(makeUpgradeDisconnection); | ||
|
||
// TODO: Simplify once we have @endo/patterns (or just export the shape). | ||
// const upgradeDisconnectionShape = harden({ | ||
// name: 'vatUpgraded', | ||
// upgradeMessage: M.string(), | ||
// incarnationNumber: M.number(), | ||
// }); | ||
// const isUpgradeDisconnection = err => matches(err, upgradeDisconnectionShape); | ||
/** | ||
* @param {any} err | ||
* @returns {err is DisconnectionObject} | ||
*/ | ||
export const isUpgradeDisconnection = err => | ||
isObject(err) && | ||
err.name === 'vatUpgraded' && | ||
typeof err.upgradeMessage === 'string' && | ||
typeof err.incarnationNumber === 'number'; | ||
harden(isUpgradeDisconnection); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// @ts-check | ||
import '@endo/init'; | ||
import test from 'ava'; | ||
import { | ||
makeUpgradeDisconnection, | ||
isUpgradeDisconnection, | ||
} from '../src/upgrade-api.js'; | ||
|
||
test('isUpgradeDisconnection must recognize disconnection objects', t => { | ||
const disconnection = makeUpgradeDisconnection('vat upgraded', 2); | ||
t.true(isUpgradeDisconnection(disconnection)); | ||
}); | ||
|
||
test('isUpgradeDisconnection must recognize original-format disconnection objects', t => { | ||
const disconnection = harden({ | ||
name: 'vatUpgraded', | ||
upgradeMessage: 'vat upgraded', | ||
incarnationNumber: 2, | ||
}); | ||
t.true(isUpgradeDisconnection(disconnection)); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,64 @@ | ||
import { E, Far } from '@endo/far'; | ||
import { isObject } from '@endo/marshal'; | ||
import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js'; | ||
|
||
import './types-ambient.js'; | ||
|
||
const { details: X, Fail } = assert; | ||
const sink = () => {}; | ||
|
||
/** | ||
* Check the promise returned by a function for rejection by vat upgrade, | ||
* and refetch upon encountering that condition. | ||
* | ||
* @template T | ||
* @param {() => ERef<T>} getter | ||
* @param {ERef<T>[]} [seed] | ||
* @returns {Promise<T>} | ||
*/ | ||
const reconnectAsNeeded = async (getter, seed = []) => { | ||
let disconnection; | ||
let lastVersion = -Infinity; | ||
// End synchronous prelude. | ||
await null; | ||
for (let i = 0; ; i += 1) { | ||
try { | ||
const resultP = i < seed.length ? seed[i] : getter(); | ||
// eslint-disable-next-line no-await-in-loop, @jessie.js/no-nested-await | ||
const result = await resultP; | ||
return result; | ||
} catch (err) { | ||
if (isUpgradeDisconnection(err)) { | ||
if (!disconnection) { | ||
disconnection = err; | ||
} | ||
const { incarnationNumber: version } = err; | ||
if (version > lastVersion) { | ||
// We don't expect another upgrade in between receiving | ||
// a disconnection and re-requesting an update, but must | ||
// nevertheless be prepared for that. | ||
lastVersion = version; | ||
continue; | ||
} | ||
} | ||
// if `err` is an (Error) object, we can try to associate it with | ||
// information about the disconnection that prompted the request | ||
// for which it is a result. | ||
if (isObject(err) && disconnection && disconnection !== err) { | ||
try { | ||
assert.note( | ||
err, | ||
X`Attempting to recover from disconnection: ${disconnection}`, | ||
); | ||
} catch (_err) { | ||
// eslint-disable-next-line no-empty | ||
} | ||
} | ||
throw err; | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Create a near iterable that corresponds to a potentially far one. | ||
* | ||
|
@@ -26,25 +81,48 @@ export const subscribe = itP => | |
* longer needed so they can be garbage collected. | ||
* | ||
* @template T | ||
* @param {ERef<PublicationRecord<T>>} pubList | ||
* @param {ERef<EachTopic<T>>} topic | ||
* @param {ERef<PublicationRecord<T>>} nextCellP | ||
* PublicationRecord corresponding with the first iteration result | ||
* @returns {ForkableAsyncIterator<T, T>} | ||
*/ | ||
const makeEachIterator = pubList => { | ||
const makeEachIterator = (topic, nextCellP) => { | ||
// To understand the implementation, start with | ||
// https://web.archive.org/web/20160404122250/http://wiki.ecmascript.org/doku.php?id=strawman:concurrency#infinite_queue | ||
return Far('EachIterator', { | ||
next: () => { | ||
const resultP = E.get(pubList).head; | ||
pubList = E.get(pubList).tail; | ||
// We expect the tail to be the "cannot read past end" error at the end | ||
// of the happy path. | ||
// Since we are wrapping that error with eventual send, we sink the | ||
// rejection here too so it doesn't become an invalid unhandled rejection | ||
// later. | ||
void E.when(pubList, sink, sink); | ||
const { | ||
head: resultP, | ||
publishCount: publishCountP, | ||
tail: tailP, | ||
} = E.get(nextCellP); | ||
|
||
// If tailP is broken by upgrade, we will need to re-request it | ||
// directly from `topic`. | ||
const getSuccessor = async () => { | ||
const publishCount = await publishCountP; | ||
assert.typeof(publishCount, 'bigint'); | ||
const successor = await E(topic).subscribeAfter(publishCount); | ||
const newPublishCount = successor.publishCount; | ||
if (newPublishCount !== publishCount + 1n) { | ||
Fail`eachIterator broken by gap from publishCount ${publishCount} to ${newPublishCount}`; | ||
} | ||
return successor; | ||
}; | ||
|
||
// Replace nextCellP on every call to next() so things work even | ||
// with an eager consumer that doesn't wait for results to settle. | ||
nextCellP = reconnectAsNeeded(getSuccessor, [tailP]); | ||
|
||
// Avoid unhandled rejection warnings here if the previous cell was rejected or | ||
// there is no further request of this iterator. | ||
// `tailP` is handled inside `reconnectAsNeeded` and `resultP` is the caller's | ||
// concern, leaving only `publishCountP` and the new `nextCellP`. | ||
void E.when(publishCountP, sink, sink); | ||
void E.when(nextCellP, sink, sink); | ||
return resultP; | ||
}, | ||
fork: () => makeEachIterator(pubList), | ||
fork: () => makeEachIterator(topic, nextCellP), | ||
}); | ||
}; | ||
|
||
|
@@ -53,7 +131,10 @@ const makeEachIterator = pubList => { | |
* provides "prefix lossy" iterations of the underlying PublicationList. | ||
* By "prefix lossy", we mean that you may miss everything published before | ||
* you ask the returned iterable for an iterator. But the returned iterator | ||
* will enumerate each thing published from that iterator's starting point. | ||
* will enumerate each thing published from that iterator's starting point | ||
* up to a disconnection result indicating upgrade of the producer | ||
* (which breaks the gap-free guarantee and therefore terminates any active | ||
* iterator while still supporting creation of new iterators). | ||
* | ||
* If the underlying PublicationList is terminated, that terminal value will be | ||
* reported losslessly. | ||
|
@@ -64,8 +145,8 @@ const makeEachIterator = pubList => { | |
export const subscribeEach = topic => { | ||
const iterable = Far('EachIterable', { | ||
[Symbol.asyncIterator]: () => { | ||
const pubList = E(topic).subscribeAfter(); | ||
return makeEachIterator(pubList); | ||
const firstCellP = reconnectAsNeeded(() => E(topic).subscribeAfter()); | ||
return makeEachIterator(topic, firstCellP); | ||
Comment on lines
+148
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is really clear! |
||
}, | ||
}); | ||
return iterable; | ||
|
@@ -95,9 +176,10 @@ const cloneLatestIterator = (topic, localUpdateCount, terminalResult) => { | |
return terminalResult; | ||
} | ||
|
||
// Send the next request now, skipping past intermediate updates. | ||
const { value, updateCount } = await E(topic).getUpdateSince( | ||
localUpdateCount, | ||
// Send the next request now, skipping past intermediate updates | ||
// and upgrade disconnections. | ||
const { value, updateCount } = await reconnectAsNeeded(() => | ||
E(topic).getUpdateSince(localUpdateCount), | ||
); | ||
// Make sure the next request is for a fresher value. | ||
localUpdateCount = updateCount; | ||
|
@@ -161,8 +243,9 @@ const makeLatestIterator = topic => cloneLatestIterator(topic); | |
* By "lossy", we mean that you may miss any published state if a more | ||
* recent published state can be reported instead. | ||
* | ||
* If the underlying PublicationList is terminated, that terminal value will be | ||
* reported losslessly. | ||
* If the underlying PublicationList is terminated by upgrade of the producer, | ||
* it will be re-requested. All other terminal values will be losslessly | ||
* propagated. | ||
* | ||
* @template T | ||
* @param {ERef<LatestTopic<T>>} topic | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do have
@endo/patterns
now.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first attempt didn't quite work, so I'm deferring this until we've got a little more time to spend on it.