-
Notifications
You must be signed in to change notification settings - Fork 56
Can we improve the callActivity()
and callSubOrchestrator()
experiences in orchestrations?
#418
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
Comments
I'm wondering if it makes more sense to flip the order from this: context.df.callActivity(helloActivity, 'Tokyo') To something like this: helloActivity.call(context, 'Tokyo') |
@ejizba I would be in favor of that solution, and I don't immediately see a reason we couldn't do this |
context.df.callActivity()
experience in orchestrations?callActivity()
and callSubOrchestrator()
experiences in orchestrations?
I think that's a really cool idea, @ejizba. I would take it a step further and ask whether it would be possible to call it like this: yield helloActivity.call('Tokyo') // let's not forget the `yield` is necessary. Or taking it to the logical extreme and just do yield helloActivity('Tokyo') // let's not forget the `yield` is necessary. Overall, I think that hiding the |
@davidmrdavid I really like your last proposal 😍 Makes writing orchestrations one step closer to feeling like writing just normal code. Once/if we also fix #5, this would basically look exactly the same as a normal async function |
How would this affect code organization? If I want to access const subOrchestrator = df.app.orchestration('subOrchestrator', /* impl */);
const orchestrator: OrchestrationHandler = function* (context) {
const outputs = [];
outputs.push(yield context.df.callOrchestration(subOrchestrator , 'Tokyo'));
outputs.push(yield context.df.callOrchestration(subOrchestrator , 'Seattle'));
outputs.push(yield context.df.callOrchestration(subOrchestrator , 'Cairo'));
return outputs;
};
df.app.orchestration('durableOrchestrator1', orchestrator); |
@jviau: let me try to answer your questions in parts. (1)
I think a user would be able to export and import the // fileA.js
const helloActivity = df.app.activity("MyActivityName", /* impl */ );
export helloActivity;
// fileB.js
import { helloActivity } from "fileB.js"; // or something like that So I think the code organization bit is not the difficult part, we would just be re-using standard mechanisms in the language. I think the tricky bit to get right is not requiring an explicit reference to the (2)
I think I need more context here. Can you please elaborate what you're asking? |
My code sample shows what I am asking. Can I take the same approach with using the return value of |
Yes, I think so. I don't see why this would be specific to Activities. I think it would apply to Activities, and SubOrchestrators. I still need to think about how this would work with entities, which allow for fire-and-forget ops and I'm not sure how I want to control for that. So you could have an orchestrator like this: const subOrchestrator = df.app.orchestration('subOrchestrator', /* impl */);
const helloActivity = df.app.activity(activityName, { handler: (input: string) => {
return `Hello, ${input}`;
}
});
const orchestrator: OrchestrationHandler = function* (context) {
const outputs = [];
// the context is preserved by the implicit TaskOrchestrationHandler that receives each `yield`
outputs.push(yield helloActivity ('Tokyo'));
outputs.push(yield subOrchestrator('Seattle'));
return outputs;
};
df.app.orchestration('durableOrchestrator1', orchestrator); |
Fire & forget on sub-orchestrations too. I think the returned object const subOrchestrator = df.app.orchestration('subOrchestrator', /* impl */);
const helloActivity = df.app.activity(activityName, { handler: (input: string) => {
return `Hello, ${input}`;
}
});
const orchestrator: OrchestrationHandler = function* (context) {
const outputs = [];
// the context is preserved by the implicit TaskOrchestrationHandler that receives each `yield`
outputs.push(yield helloActivity('Tokyo', { retry: /* add retry details */ }));
outputs.push(yield subOrchestrator('Seattle', { retry, instanceId, fireAndForget })); // mind my rough / invalid JS here
return outputs;
};
df.app.orchestration('durableOrchestrator1', orchestrator); |
I like it, and thanks for the callout on fire-and-forget subOrchs. |
@ejizba: any feedback on this most recent suggestion? Essentially, we're making the invocation of activities, entities, and sub-orchestrators be as minimal as possible by removing the |
I just want to push back on this goal because orchestrations aren't normal code, right? In which case I do think it's helpful to have some differentiating characteristics
I'm not entirely following this and it's my biggest confusion with the newer code samples. Why was context required before and why is it magically gone now? |
I agree on hiding the context not being that beneficial. I think there is a point to keeping
This example puts it together in what I hope is valid (or at least semi valid) code. interface OrchestrationRequest<TInput, TResult> { // TResult lets the context.df.dispatch be strongly typed for its return value
Name: string;
Input: TInput;
}
// these types are different only to satisfy different overloads of `context.df.dispatch(..)`
interface ActivityRequest<TInput, TResult> { // TResult lets the context.df.dispatch be strongly typed for its return value
Name: string;
Input: TInput;
}
const subOrchestrator = df.app.orchestration('subOrchestrator', { handler: (context: ???, input: MyInput) => { /* impl, no return */ }}); // subOrchestrator is `function (input: MyInput): OrchestrationRequest<MyInput, Void>`
const helloActivity = df.app.activity(activityName, { handler: (input: string) => { // helloActivity is `function (input: string): ActivityRequest<string, string>`
return `Hello, ${input}`;
}
});
const orchestrator: OrchestrationHandler = function* (context) {
// the context is preserved by the implicit TaskOrchestrationHandler that receives each `yield`
string result = yield context.df.dispatch(helloActivity("input")); // due to the `TResult` of `ActivityRequest`, we know the result is `string` here.
yield context.df.dispatch(subOrchestrator({ /* instance of MyInput */ }), { retry, instanceId, fireAndForget }); // due to the `TResult` of `OrchestrationRequest`, we know the result is `Void` here.
return result;
};
df.app.orchestration('durableOrchestrator1', orchestrator); The Mediator pattern is what I intend to implement in the C# SDK eventually. Also I am not married to |
Personally, I find that one of the biggest selling points about durable is that it makes calling serverless functions and managing their state feel like calling regular functions and assigning regular variables. Imo, the closer orchestration code feels to normal code, the better. But I don't think this is a risk anyway since all the generator
Well, I don't think context was really required before, it was just where the API lived, but context properties weren't necessary for producing the return value of df.app.orchestration('durableOrchestrator1', function* (context) {
const outputs = [];
const retryOptions = new df.RetryOptions(1000, 2);
outputs.push(yield helloActivity({ input: 'Tokyo', retryOptions }));
outputs.push(yield helloActivity({ input: 'Seattle', retryOptions }));
outputs.push(yield helloActivity({ input: 'Cairo' }));
return outputs;
});
const helloActivity = df.app.activity(activityName, {
handler: (input) => {
return `Hello, ${input}`;
},
}); |
Good to see we have a prototype :) As for whether DF should or shouldn't feel like regular code - that's tricky and a potentially very long running conversation, so I want to make sure we don't get lost in the meta conversation here. To keep my response short: there is definitely value in highlighting that DF isn't regular code, due to code constraints and such. However, I also want to let the Node team evolve the programming model in a way that suites them best so I don't want to constraint them to how other PLs decided to convey Activity/Entity/SubOrch dispatching. To me, the question is whether that is already conveyed well enough through the I would like more thoughts on this latest prototype. I'll ask in our internal demo chat to get a few more eyes on this thread, I'd love to see what others think. |
Question on this line of code: outputs.push(yield helloActivity({ input: 'Tokyo', retryOptions })); Which of the two following ways is more idiomatic in JS/TS? (Legit asking - I don't know myself). // option 1: input is always first param, options (for configuring retry, etc) is an optional second param.
function someActivity(input: TInput, options?: TaskOptions): TResult // is this how you do optional parameters in TS?
// option 2: two overloads. 1 for just input, 1 for a complex object which combines input and options
function someActivity(input: TInput): TResult // only input, no options.
function someActivity(options: TaskOptions<TInput>): TResult // input is a part of options object. |
I think all of our suggestions are moving towards a strongly-typed function generated from the |
Closed by #535 |
Uh oh!
There was an error while loading. Please reload this page.
(see relevant discussion: ejizba/func-nodejs-prototype#14 (comment))
Currently, if an orchestrator wants to call an activity function, it would have code like this:
passing in the activity's name as a string. This provides no intellisense and no validation to confirm that the activity name is the same as the registered activity function.
Similarly, suborchestrations are started by specifying the orchestration's name as a string:
This issue is to explore whether the new programming model can provide us an opportunity to improve this
callActivity()
andcallSubOrchestrator()
experience, and whether such an improvement would be worth the engineering cost.An example of this would be to have the API to register activities (
df.app.activity()
) and orchestrations (df.app.orchestration()
) return some kind of object that can be passed to thecallActivity()
/callSubOrchestration()
functions, as such:The text was updated successfully, but these errors were encountered: