Azure Functions: Orchestrator and Activity best practice

Viewed 28

Good day,

My question is mostly about best practices for using the Activity and Orchestrator functions. Haven't found the answer here - Microsoft docs

In short - I have a simple orchestrator that:

  • retrieves some list of objects from an API (using Activity)
  • processes these items (filters and sorts) (directly in Orchestrator)
  • sends processed items to another API (using Activity)

Question: both API calls in Orchestrator (await context.CallActivityAsync()) is asynchronous and takes some time to execute. Functions framework unloads orchestrator (I'm on consumption plan) from memory and replays from the beginning (which is expected). Every replay orchestrator gets saved (if executed already) state on vars from context.CallActivityAsync() calls, BUT not from local vars (e.g. objectsStartWithA, objectsStartWithB and filteredObjects from example below). It doesn't break anything but does the same things on every replay (if any) over and over again. Do I need to extract all business logic to Activities and call them using await context.CallActivityAsync()?

namespace Demo.Functions
{
    public class FunctionClass
    {
        public const string FunctionName = "Function1";

        private readonly ILogger<FunctionClass> _logger;

        public FunctionClass(
            ILogger<FunctionClass> logger)
        {
            _logger = logger;
        }

        [FunctionName(FunctionName)]
        public async Task ExecuteAsync([OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var logger = context.CreateReplaySafeLogger(_logger);
            logger.LogInformation("Process started");

            var someObjects =
               (await context.CallActivityAsync<IEnumerable<ApiObject>>("GetApiObjectsAsync", null)).ToList();

            var objectsStartWithA =
                someObjects
                        .Where(x => !string.IsNullOrEmpty(x.Name) && x.Name.StartsWith("a", StringComparison.OrdinalIgnoreCase))
                        .OrderBy(x => x.Name)
                        .ToList();

            var objectsStartWithB =
                someObjects
                        .Where(x => !string.IsNullOrEmpty(x.Name) && x.Name.StartsWith("b", StringComparison.OrdinalIgnoreCase))
                        .OrderBy(x => x.Name)
                        .ToList();

            var objectsToCompare = objectsStartWithA.Union(objectsStartWithB);


            //Long-running api request
            var someOtherObjects =
               (await context.CallActivityAsync<IEnumerable<ApiObject>>("GetOtherApiObjectsAsync", null)).ToList();

            var filteredObjects =
                someOtherObjects
                        .Where(x => objectsToCompare.All(z => z.Id != z.Id))
                        .OrderBy(x => x.Id)
                        .ToList();


            //Saving
            await context.CallActivityAsync("SaveObjects", filteredObjects);

            logger.LogInformation("Process finished");
        }
    }

    public class ApiObject
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

Thank you!

1 Answers

I would push those into an Activity and keep the orchestrator purely just coordinating the various activities. In the example you've given, I'd just change GetApiObjectsAsync activity to return the data in the filtered form you want, rather than introduce another activity call tbh. IMHO, that would just incur additional/unnecessary steps in the orchestration workflow & state storage that you don't need.

Maybe your example is simplified, but looks like you only care about objectsToCompare - which is objects starting with 'a' or 'b'. So, a single filter applied in your existing activity function before returning, would do the job.

Related