Azure / azure-functions-durable-js

JavaScript library for using the Durable Functions bindings
https://www.npmjs.com/package/durable-functions
MIT License
128 stars 46 forks source link

Improve calling activities and suborchestrators #535

Closed hossam-nasr closed 9 months ago

hossam-nasr commented 11 months ago

Resolves #418. This makes it such that the registeration functions app.orchestration() and app.activity() return a RegisteredOrchestration and RegisteredActivity, respectively, which can be called to return a yieldable Task, without having to call context.df.callSubOrchestrator(), context.df.callSubOrchestratorWithRetry(), context.df.callActivity(), or context.df.callActivityWithRetry().

Instead, you can call:

yield mySubOrchestrator(input);
yield mySubOrchestrator(input).withRetry(retryOptions);
yield myActivity(input);
yield myActivity(input).withRetry(retryOptions);

with mySubOrchestraor and myActivity being the result of registering the orchestration/activity. Detailed examples below:

Calling activities and suborchestrators:

Before

const helloActivityHandler: ActivityHandler = function (input: unknown): string {
    return `Hello ${input}`;
};
df.app.activity(helloActivityName, {
    handler: helloActivityHandler,
});

const sayHelloWithActivityHandler: OrchestrationHandler = function* (context: OrchestrationContext) {
    const input: unknown = context.df.getInput();
    const output: string = yield context.df.callActivity(helloActivityName, input);
    return output;
};
df.app.orchestration("sayHelloWithActivity", sayHelloWithActivityHandler);

const sayHelloWithSubOrchestratorHandler: OrchestrationHandler = function* (
    context: OrchestrationContext
) {
    const input: unknown = context.df.getInput();
    const output: string = yield context.df.callSubOrchestrator("sayHelloWithActivity", input);
    return output;
};
df.app.orchestration("sayHelloWithSubOrchestrator", sayHelloWithSubOrchestratorHandler);

After

const helloActivityHandler: ActivityHandler = function (input: unknown): string {
    return `Hello ${input}`;
};
const helloActivity: RegisteredActivity = df.app.activity(helloActivityName, {
    handler: helloActivityHandler,
});

const sayHelloWithActivityHandler: OrchestrationHandler = function* (context: OrchestrationContext) {
    const input: unknown = context.df.getInput();
    const output: string = yield helloActivity(input);
    return output;
};
const sayHelloWithActivityOrchestrator: RegisteredOrchestration = df.app.orchestration("sayHelloWithActivity", sayHelloWithActivityHandler);

const sayHelloWithSubOrchestratorHandler: OrchestrationHandler = function* (
    context: OrchestrationContext
) {
    const input: unknown = context.df.getInput();
    const output: string = yield sayHelloWithActivityOrchestrator(input);
    return output;
};
df.app.orchestration("sayHelloWithSubOrchestrator", sayHelloWithSubOrchestratorHandler);

With Retry:

Before

const flakyFunction: ActivityHandler = function (_input: any, context: InvocationContext): void {
    context.log("Flaky Function Flaking!");
    throw new Error("FlakyFunction flaked");
};
df.app.activity("flakyFunction", {
    handler: flakyFunction,
});

const callActivityWithRetry: OrchestrationHandler = function* (context: OrchestrationContext) {
    const retryOptions: RetryOptions = new df.RetryOptions(1000, 2);

    let returnValue: any;
    try {
        returnValue = yield context.df.callActivityWithRetry("flakyFunction", retryOptions);
    } catch (e) {
        context.log("Orchestrator caught exception. Flaky function is extremely flaky.");
    }

    return returnValue;
};
df.app.orchestration("callActivityWithRetry", callActivityWithRetry);

After

const flakyFunction: ActivityHandler = function (_input: any, context: InvocationContext): void {
    context.log("Flaky Function Flaking!");
    throw new Error("FlakyFunction flaked");
};
const flakyActivity: RegisteredActivity = df.app.activity("flakyFunction", {
    handler: flakyFunction,
});

const callActivityWithRetry: OrchestrationHandler = function* (context: OrchestrationContext) {
    const retryOptions: RetryOptions = new df.RetryOptions(1000, 2);

    let returnValue: any;
    try {
        returnValue = yield flakyActivity().withRetry(retryOptions);
    } catch (e) {
        context.log("Orchestrator caught exception. Flaky function is extremely flaky.");
    }

    return returnValue;
};
df.app.orchestration("callActivityWithRetry", callActivityWithRetry);
davidmrdavid commented 11 months ago

Just to confirm, in this example:

const flakyFunction: ActivityHandler = function (_input: any, context: InvocationContext): void {
    context.log("Flaky Function Flaking!");
    throw new Error("FlakyFunction flaked");
};
const flakyActivity: RegisteredActivity = df.app.activity("flakyFunction", {
    handler: flakyFunction,
});

const callActivityWithRetry: OrchestrationHandler = function* (context: OrchestrationContext) {
    const retryOptions: RetryOptions = new df.RetryOptions(1000, 2);

    let returnValue: any;
    try {
        returnValue = yield (flakyActivity().withRetry(retryOptions));
    } catch (e) {
        context.log("Orchestrator caught exception. Flaky function is extremely flaky.");
    }

    return returnValue;
};
df.app.orchestration("callActivityWithRetry", callActivityWithRetry);

Does the line

        returnValue = yield (flakyActivity().withRetry(retryOptions));

need the parentheses around the activity task, or could we have?

        returnValue = yield flakyActivity().withRetry(retryOptions);

I'm just trying to understand if there's an order of evaluation problem here, which would explain the parenthesis. Just checking!

hossam-nasr commented 11 months ago

@davidmrdavid No it is not. I had that there first just because I was debugging something and thought that the precedence order might be causing it, but it turns out it wasn't. I just double checked and the parentheses are not required. I also update the PR description now.