Durable Function App Series: Chaining

01 May 2020
#Azure#Function App#Guide

Series Links

Durable Functions: Series Intro Durable Functions: Function Chaining Durable Functions: Fan In/Out - (coming soon)

A note about Orchestrator Functions

At a fundamental level Orchestrators are generator functions wrapped in the Durable Function runtime and when used with Durable Functions, generators allow for a deterministic execution with a given context, when following the code constraints. This contributes to why there are constraints around Orchestrators Functions when it comes to I/O and async processing, but there are a number of Orchestrator safe utility functions available within an Orchestrator context. Generators were chosen over other mechanisms due the differences in async/await behaviour in C#, but the specific reasoning is explained in better detail on this issue comment. The main take away is that while they appear to be like normal JavaScript functions, they do execute and behave differently, some care should be taken when testing/designing for more complex scenarios.

Function Chaining

This pattern is defined as an execution of a sequence of functions in a particular order, with output of one execution of, potentially, applied to the next function. The examples provided don't appear to demonstrate true chaining, but an ordered execution, so in the next post I'll offer some examples for what chaining could be.

The docs show a simple Orchestrator function, E1_HelloSequence, that calls a few Hello World type of activity functions, E1_SayHello, that will return a string. There are some callouts through the docs on to how to use each type of function, along with the gotchas for Orchestrator functions, This example illustrates how simple it is to call a series of activity functions and return the outputs to a client.

  const output = [];
  output.push(yield context.df.callActivity("E1_SayHello", "Tokyo"));
  output.push(yield context.df.callActivity("E1_SayHello", "Seattle"));
  output.push(yield context.df.callActivity("E1_SayHello", "London"));
  return output;

An alternative example without an array would look like, but is really just a more of convenient output shape.

  const tokyoMessage = yield context.df.callActivity("E1_SayHello", "Tokyo");
  const seattleMessage = yield context.df.callActivity("E1_SayHello", "Seattle");
  const londonMessage = yield context.df.callActivity("E1_SayHello", "London");
  return { tokyoMessage, seattleMessage, londonMessage }

5x Function Chaining

So you want to do take chaining up a few notches, here is an example that will create and queue an alert if the store rating is below a certain threshold; the next example post will take this example and utilize Fan Out/In with multiple Orchestrator functions.

  /**  A1_GetStoreInfo will look for store based on some input
   *   and return the object representation of the store
   */
  const store = yield context.df.callActivity('A1_GetStoreInfo', storeLocation);
  // A1_GetUsers will look for employees of a given store and return a store ratin
  const rating = yield context.df.callActivity('A1_GetRating', store);

  // If the stores at or below the threshold, create alert and send to queue
  if (rating <= threshold) {
    const alert = yield context.df.callActivity('A1_BuildAlert', { store, rating, threshold });

    // Since we don't require the output of the activity,
    // we do not need to store the response, if there is one
    yield context.df.callActivityWithRetry('A1_QueueAlert', retryOpts, alert);
  }

Tips

When looking at the example code I've linked to, the code is partially separated from the core components for shared functionality/testing or to keep the source of your durable function abstracted and manageable in another module; in practice I choose to do this in order maintain a simple function declaration and maintain logic for each step separated. By splitting them out into smaller components, it's easier to discover reusable components and supports better readability and maintainability. For larger projects that have many complex moving parts, TypeScript becomes increasingly valuable as well over plain JavaScript, especially when changing execution contexts and shapes of the input and output of many functions.

Example

// index.js
module.exports = df.orchestrator(function* (context) {
  // function body
})

vs

// index.js
const myGenFunction = require('module.js')
module.exports = df.orchestrator(myGenFunction)

// module.js
function* myGenFunction(context) {
  // function body
}
module.exports = myGenFunction

Next

On deck is the Fan In/Out example provided here, which the expanded focus will be on using orchestrator safe utilities for I/O & async-like requests and sub orchestrations