A key concept for managing foreach
in Durable functions, whether that be Function Chaining or Fan-in/Fan-out is that the data to iterate is returned from an Activity
and that the processing of each data item is also performed within the context of an Activity.
This pattern will ensure that your logic is deterministic, don't rely on the NonDeterministicOrchestrationException
as your proof that the logic is deterministic, that is commonly raised when a replay operation sends a different input than was expected and may not directly or initially inform you of non-deterministric logic.
Any call to the database or an external service or other http endpoints should be considered non-deterministic, so wrap code like that inside of an Activity. This way when the Orchestrator is replaying, it will retrieve the results of previous completed call to that activity form the underlying store.
- This can help improve performance if the logic in the activity is only evaluated once for the durable lifetime.
- This will also protect you from transient errors that may ocurr if during a replay attempt the underlying provider may be momentarily unavailable.
In the following simple example we have a rollover function that needs to be performed on many facilities, we can use fan-out to perform the individual tasks for each facility concurrently or chaining sequentially:
[FunctionName("RolloverBot")]
public static async Task<bool> RolloverBot(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
// example of how to get data to iterate against in a determinist paradigm
var facilityIds = await context.CallActivityAsync<int[]>(nameof(GetAllFacilities), null);
#region Fan-Out
var tasks = new List<Task>();
foreach (var facilityId in facilityIds)
{
tasks.Add(context.CallActivityAsync(nameof(RolloverFacility), facilityId));
}
// Fan back in ;)
await Task.WhenAll(tasks);
#endregion Fan-Out
#region Chaining / Iterating Sequentially
foreach (var facilityId in facilityIds)
{
await context.CallActivityAsync(nameof(RolloverFacility), facilityId);
}
#endregion Chaining / Iterating Sequentially
return true;
}
/// <summary>
/// Return a list of all FacilityIds to operate on
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[FunctionName("GetAllFacilities")]
public static async Task<int[]> GetAllFacilities([ActivityTrigger] IDurableActivityContext context)
{
var db = await Globals.GetDataContext();
var data = await db.Facilities.AddQueryOption("$select", "Id").ExecuteAsync();
return data.Where(x => x.Id.HasValue).Distinct().Select(x => x.Id.Value).ToArray();
}
[FunctionName("RolloverFacility")]
public static async Task<bool> RolloverFacility(
[ActivityTrigger] IDurableActivityContext context)
{
int facilityId = context.GetInput<int>();
bool result = false;
... insert rollover logic here
result = true;
return result;
}
In this way, even if your Activity logic uses System.Random
, Guid.CreateNew
or DateTimeOffset.Now
to determine the the facilityIds
to return, the durable function itself is still considered Deterministic and will replay correctly.
As a rule I would still recommend passing through the IDurableOrchestrationContext.CurrentUtcDateTime
from the orchestration function to the activity if your activity logic is time dependent as it makes the logic more obvious that the Orchestrator is actually controlling the tasks, and not the other way around, also there can be a miniscule lag time due to the function implementation between the scheduling of CallActivityAsync
and its actual execution.