The following design emerged from using Azure Durable Functions with lots of fan out, which turned out to not work very well. Our starting point was an Orchestrator with a companion Durable Entity for state keeping, and Activity functions for the actual work.
We ran into several problems, like timeouts and too many ongoing instances.
Finally, we discovered that these three artifacts could be merged into one Durable Entity which calls itself:
- The entity keeps the state, the Context
- Operations are modeled as states as in the State Design Pattern
- Each state takes a Context, do something useful, then return the next version of the Context.
This turned out to work very well, but wasn't maintainable because every state need to know its successor. Which means for every new state added, we had to identify its predecessor and change it to point to the new state as its successor.
In the end, we don't really care about predecessors and successors, only that functions can be called one by one in correct order.
I don't know what to call the elements, so Context
and Pipeline
will do for now. First version:
// Convert 212 Fahrenheit to Celsius
var initial = new Context(212, 0);
var pipeline = new Pipeline(
x => x with { x.Value - 32},
x => x with { x.Value * 5},
x => x with { x.Value / 9}
);
var a = pipeline.Run(initial); // returns Context(180, 1)
var b = pipeline.Run(a); // returns Context(900, 2)
var c = pipeline.Run(b); // returns Context(100, 3)
These are the basic requirements, but we need more:
- Each step may run more than once, like when we read data from a paged api
- All steps may run more than once, like all dates for a given period
- Functions must be async for IO
Two new functions are prepended: One to decide if the whole pipeline should be run again, the other to decide if the current function should be run again:
var pipeline = new Pipeline(
x => x.Position < 3,
x => x.ContinuationToken != null,
x => x with { x.Value - 32},
x => x with { x.Value * 5},
x => x with { x.Value / 9}
);
All functions must be async, so we can:
var next = await pipeline.Run(previous);
Finally, the pipeline should be generic and accept yet another function to advance the context to point to the next function.
var pipeline = new Pipeline<Context>(
x => x.Position < 3,
x => x.ContinuationToken != null,
x => x with { Position = x.Position + 1},
...
);