-
Notifications
You must be signed in to change notification settings - Fork 208
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(orchestration): return unwrapped vows #9454
Conversation
4735d0d
to
5c73a22
Compare
Deploying agoric-sdk with Cloudflare Pages
|
), | ||
provideICQConnection: M.callWhen(M.string()).returns( | ||
M.remotable('Connection'), | ||
makeAccount: M.call(M.string(), M.string()).returns(M.promise()), |
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.
why lose the validation of the return type?
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.
makeAccount
is no longer an async function, so I didn't think .callWhen()
was appropriate. Reverted in 1471410
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.
I believe that .callWhen
is not simple validation and indeed make the function become async. @erights for confirmation, but we may need to manually validate this, or find a way to make this pattern check compatible with upgradable methods.
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.
Isn't the problem whether an await
happens in the method? If the JS code that this is guarding doesn't have the async
keyword then it can't await.
How could the guard change that?
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.
The problem is doing any promise "then" if the promise may not be resolved until a future crank. Whether that's done by an await
, by a when
or any other mechanism that under the hood wraps promise.then()
. callWhen
is such a mechanism as, from what I understand, it internally when
the arguments and delays the invocation of the user function until after the arguments have settled.
), | ||
}); | ||
|
||
const requestICAConnectionWatcherI = M.interface( |
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.
since these aren't exported, I think they would read better inlined..
if you don't then please capitalize the way other interface guards are, StudlyCaps
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.
await E(port).connect( | ||
remoteConnAddr, | ||
chainAccountKit.connectionHandler, | ||
const portAllocator = getPower(this.state.powers, 'portAllocator'); |
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.
out of scope, but what motivates this getPower abstraction? it makes it harder to review.
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.
There's a comment here:
agoric-sdk/packages/orchestration/src/service.js
Lines 29 to 31 in 19bc874
* PowerStore is used so additional powers can be added on upgrade. See | |
* [#7337](https://github.com/Agoric/agoric-sdk/issues/7337) for tracking on Exo | |
* state migrations. |
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.
thanks, that makes sense. I was looking on getPower.
So that explains why a store instead of a record, and I take it the getter is because maps assume homogeneity of value types. That could be worthwhile to include as a comment.
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.
btw, going forward s/7337/7407 #7337 (comment)
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 talked about getPower and how to get rid of it, right, @0xpatrickdev ? (IOU link)
we don't need to complicate access to known state fields to provide for expansion.
we just need one reserved-for-future-use property
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.
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.
Thanks @dckc . I'll tackle this in a follow-up PR
return when( | ||
watch( |
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.
if we're always going to have to when(watch(
is there a worthwhile convenience function to make? It's fine to chain them but it ends up increasing the indentation.
maybe a helper facet could manage it? perhaps validated automatically against the watcher facet
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.
I think we ultimately only want to return watch
. when
is a stop gap during the refactor so we still return a promise.
import { makeDurableZone } from '@agoric/zone/durable.js'; | ||
import { prepareOrchestrationTools } from './service.js'; | ||
|
||
/** @import {OrchestrationPowers} from './service.js' */ | ||
|
||
export const buildRootObject = (_vatPowers, _args, baggage) => { | ||
const zone = makeDurableZone(baggage); | ||
const vowTools = prepareVowTools(zone.subZone('VowTools')); |
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.
is the subzone necessary?
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.
I'm not sure. I was following the example here:
agoric-sdk/packages/vow/README.md
Lines 57 to 77 in 1b838d2
## 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: | |
```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 { 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. | |
``` |
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.
At least for the network and ibc vats, we've followed the pattern of creating a subzone specifically for all vow-related data
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.
Why? What happens if you don't make a subzone?
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.
Reviewing this caused me to look a little deeper into the vow implementation, which raised some questions for @michaelfig , but I took those offline.
My main concern is with how synchronous errors now end up in throws instead of vow rejections.
async close() { | ||
/// XXX what should the behavior be here? and `onClose`? | ||
close() { | ||
/// TODO #9192 what should the behavior be here? and `onClose`? | ||
// - retrieve assets? | ||
// - revoke the port? | ||
const { connection } = this.state; | ||
if (!connection) throw Fail`connection not available`; |
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.
Ok this is interesting. A good API design is to not throw synchronously if you return a promise. Previously because the function was async
, the call would return a rejected promise with this error. The equivalent would be to return a rejected vow, but in general, this is very cumbersome to get right all the time. I am wondering if we don't need to revive something like the promise constructor pattern and wrap all synchronous prelude into a helper.
Something along the lines of
/**
* @template {any} T
* @param {(...args: any[]) => Vow<Awaited<T>> | Awaited<T>} fn
* @returns {Vow<Awaited<T>>}
*/
const asVow = fn => {
const kit = makeVowKit();
try {
kit.resolver.resolve(fn());
} catch(e) {
kit.resolver.reject(e);
}
return kit.vow;
};
and then
close() {
return when(asVow(() => {
/// TODO #9192 what should the behavior be here? and `onClose`?
// - retrieve assets?
// - revoke the port?
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
return watch(E(connection).close());
}));
},
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.
Thanks for highlighting this. I’ve added comments in areas where we can fail synchronously referencing this comment and the parent issue in 40290c1.
I suppose the same would also apply for something like provideICQConnection
, where we can return synchronously early?
/**
* @param {IBCConnectionID} controllerConnectionId
* @returns {ICQConnection | Promise<ICQConnection>}
*/
provideICQConnection(controllerConnectionId) {
if (this.state.icqConnections.has(controllerConnectionId)) {
return this.state.icqConnections.get(controllerConnectionId)
.connection;
}
const remoteConnAddr = makeICQChannelAddress(controllerConnectionId);
const portAllocator = getPower(this.state.powers, 'portAllocator');
return when(
watch(
// allocate a new Port for every Connection
// TODO #9317 optimize ICQ port allocation
E(portAllocator).allocateICQControllerPort(),
this.facets.requestICQChannelWatcher,
{
remoteConnAddr,
controllerConnectionId,
},
),
);
},
handleExecuteEncodedTxWatcher: { | ||
/** @param {string} ack */ | ||
onFulfilled(ack) { | ||
return parseTxPacket(ack); | ||
}, | ||
}, |
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.
This doesn't look like it needs to be part of the kit. Why not have ../utils/packet.js
provide a singleton parseTxPacketWatchHandler
that can be used by all calls? But I suppose that'd still require a prepare call accepting a zone.
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.
Thanks, taking note of this pattern for the future. For now I've decided to keep the handler as a facet on the chainAccountKit
- it doesn't seem like we'll need parseTxPacket
elsewhere.
I adopted your naming suggestions in 0fc3ca1 and 438446a - but am just calling it a Watcher
and not a WatcherHandler
for brevity.
59f4de4
to
9428ec1
Compare
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.
Vow implementation looking good!
return when( | ||
watch( | ||
// allocate a new Port for every Connection | ||
// TODO #9317 optimize ICQ port allocation | ||
E(portAllocator).allocateICQControllerPort(), | ||
this.facets.requestICQConnectionWatcher, | ||
{ | ||
remoteConnAddr, | ||
controllerConnectionId, | ||
}, | ||
), |
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.
Instead of nesting the call of E(port.connect)
within requestICQConnectionWatcher
, you could instead take an approach like this:
const icqControllerPortWatcher = watch(E(portAllocator).allocateICQControllerPort(), this.facets.requestICQConnectionWatcher)
return when(watch(icqControllerPortWatcher, this.facets.portConnectWater, context))
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.
s/icqControllerPortWatcher/icqControllerPortVow/
843897c
to
99c4cae
Compare
ed28c2c
to
5fccb85
Compare
5fccb85
to
c0fd411
Compare
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.
I rebased this on master and fixed issue I ran into. One was a race condition with VBANK fake bridge. I put an eventLoop await in the failing tests. Longer term we'll have the getBalances()
checkpoint to prevent withdrawing below zero.
I also made all the changes I would have requested. So @0xpatrickdev PTAL at whether it LGTY before you merge.
); | ||
trace('undelegate response', response); | ||
const { completionTime } = response; | ||
completionTime || Fail`No completion time result ${result}`; |
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.
completionTime || Fail`No completion time result ${result}`; | |
completionTime || Fail`No completion time in result: ${result}`; |
LGTM |
- adds asVow() helper function that coerces the result of a function to a Vow - see comment from @mhofman for inspiration: #9454 (comment)
- adds asVow() helper function that coerces the result of a function to a Vow - see comment from @mhofman for inspiration: #9454 (comment)
- adds asVow() helper function that coerces the result of a function to a Vow - see comment from @mhofman for inspiration: #9454 (comment)
- adds asVow() helper function that coerces the result of a function to a Vow - see comment from @mhofman for inspiration: #9454 (comment)
refs: #9449
Description
service.js
) to return vowsSecurity Considerations
Scaling Considerations
These changes are necessary towards supporting using
orchestration
inasyncFlow
.Documentation Considerations
Testing Considerations
Upgrade Considerations