Azure Durable Functions, deel 10: eeuwig durende orchestrations.

Onze workflow is af. Alles doet het. Niets aan de hand. Mensen die zich op de website inschrijven worden nu gecontroleerd, gedeeld met andere groepen, worden beoordeeld door het bestuur van Stichting dotNed en hun aanwezigheid wordt gedeeld op Twitter (of ze het nu willen of niet: zie einde vorige post).

Er is alleen een maar: we hebben een database met potentiele attendees die goedgekeurd moeten worden. Die database vult zich langzaam aan met niet meer relevante data. Nu kunnen we dat eenvoudig oplossen door in de Azure Function die aangeroepen wordt vanuit de email de boel weg te gooien, maar ik weet een mooiere variant. Ik heb het over eeuwig durende orchestrations.

Orchestrations hebben een paar regels. Eentje hebben we al uitgebreid gesproken: ze moeten deterministisch zijn. We mogen dus niet een database uitlezen, de config uitlezen, Guid.NewGuid() aanroepen, DateTime.Now gebruiken enzovoorts. Iets wat WEL mag maar eigenlijk niet zo moeten mogen is de volgende dummy orchestrator:

[FunctionName("O_CleanUpDatabases")]
public static async void CleanUpDatabases(
    [OrchestrationTrigger] DurableOrchestrationContext ctx, 
    ILogger log)
{
    while (true)
    {
        log.LogWarning("Cleaning up the database.");
        // Call code to clean the database with old stuff
        await Task.Delay(TimeSpan.FromDays(1));
    } // And restart...
}

Op zich is hier niet zo heel veel mis mee. We starten de orchestration en die gaat de database opruimen in een of andere activity. Dan wachten een dag. En dan gaan we weer opnieuw beginnen.

Maar dit heeft een paar problemen: ten eerste kost dit geld. Als je het Consumption Plan gebruikt in Azure, betaal je alleen voor de tijd die het kost om je werk uit te voeren. Onze email verzending bijvoorbeeld wacht 24 uur (nou ja, 30 seconden in onze demo) voor hij verder gaat. Die hele 24 uur is onze workflow idle en kost ons geen geld.

Met een Task.Delay blijft de orchestration in de lucht, 24 uur lang. En daar betaal je voor.

Task.Delay is trouwens een van de calls die je niet mag doen in Durable Functions. Je kunt wel een TimerActivity maken, die doet ongeveer hetzelfde. Maar dan nog hebben we een probleem.

Event sourcing

Weet je nog die tabel in de TableStorage waar alle calls ingelogd werden? Weet je nog hoe groot die werd? Het idee erachter is dat je altijd de hele flow terug kan spelen door alle events van het begin af aan opnieuw te laten gebeuren. Dit is event sourcing: alle events, timestamps, input en output wordt opgeslagen.

Stel je nu eens voor een orchestration zoals bovenstaande, die iedere minuut iets doet. Voor altijd. Dat wordt heel veel data in de tabel. Op niets af: want je wilt nooit exact hetzelfde event afspelen van 2 jaar geleden.

Dus dat kan anders. En dat moet anders. En dat gaan we doen ook.

[FunctionName("O_CleanUpDatabases")]
public static async void CleanUpDatabases(
    [OrchestrationTrigger] DurableOrchestrationContext ctx,
    ILogger log)
{
    var counter = ctx.GetInput<int>();
    log.LogWarning($"Cleaning up the database, number {counter}.");
    // Call code to clean the database with old stuff

    counter++;
    var nextCallTime = ctx.CurrentUtcDateTime.AddSeconds(10);
    await ctx.CreateTimer(nextCallTime, CancellationToken.None);

    ctx.ContinueAsNew(counter);
}

Deze code lijkt wat ingewikkelder maar dat valt mee. Ik doe sowieso iets extra’s: ik hou bij hoe vaak dit uitgevoerd wordt. In de orchestration context heb ik nu een int waarde die die aantallen bijhoudt. Die lees ik uit in regel 6.

Vervolgens gaan we loggen en de database opruimen. Dat laatste doen we uiteraard in een activity, maar die kennen we nu wel dus ik laat het even zo. Ik verhoog daarna de counter.

Dan vraag ik aan de runtime de huidige datetime en tel daar 10 seconden bij op (dat moet uiteraard 24 uur zijn maar dat demo’ed zo lastig). Ik maak een timer aan die die tijd wacht en geef geen CancellationToken mee.

En dan roep ik ctx.ContinueAsNew(counter) aan.

Looping in een orchestration

Wat doet die call naar ContinueAsNew nu? Wel, er gebeuren een paar dingen.

Ten eerste wordt de counter variable opgeslagen. Dan verlaten we de orchestration waarna de runtime meteen een nieuwe aanmaakt. Alle data met betrekking tot de oude instance wordt weggegooid: die hebben we niet meer nodig. En dan begint hij weer van voor af aan: we hebben een nieuwe orchestration. Deze heeft een nieuwe ID, de IsReplaying is in het begin al False, en ctx.CurrentDateTime geeft ook echt ctx.CurentDateTime aan.

Als we nu meerdere activities zouden aanroepen, gelden nog steeds de regels die we al hadden: na iedere aanroep van de activity verdwijnt de orchestration even om daarna weer verder te gaan met IsReplaying = true (en ctx.CurrentUtcDateTime geeft die oude waardes terug).

Het enige verschil met de vorige run is dat counter nu 1 hoger is.

Om hem af te maken geef ik je ook even de standaard Azure Function die hem aftrapt:

public static async Task<HttpResponseMessage> StartDatabaseCleanUp(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]
    HttpRequestMessage req,
    [OrchestrationClient] DurableOrchestrationClient client,
    ILogger log)
{
    log.LogWarning("About to start the clean up method");

    var instanceId = await client.StartNewAsync("O_CleanUpDatabases", 0);
    return client.CreateCheckStatusResponse(req, instanceId);
}

De werking hiervan lijkt me duidelijk. Dit is een normale function en de runtime geeft ons de URL die we kunnen gebruiken.

Next steps

Ok. We gaan de veilige omgeving van onze emulators verlaten en we gaan deployen. Want: er is daar wel het een en ander over te zeggen, zoals monitoring en versioning. Maar: dat doen we een volgende keer!

Gepubliceerd door Dennis Vroegop

Passionate Azure Cloud Solutions Architect. I am a enthusiastic guitar player (though not as good at it as I'd like to be) and in my daytime job I teach software developers to be better at their job. Married to my wonderful wife Diana with who I try to raise our daughter Emma.

Geef een reactie

Vul je gegevens in of klik op een icoon om in te loggen.

WordPress.com logo

Je reageert onder je WordPress.com account. Log uit /  Bijwerken )

Google photo

Je reageert onder je Google account. Log uit /  Bijwerken )

Twitter-afbeelding

Je reageert onder je Twitter account. Log uit /  Bijwerken )

Facebook foto

Je reageert onder je Facebook account. Log uit /  Bijwerken )

Verbinden met %s

Deze site gebruikt Akismet om spam te bestrijden. Ontdek hoe de data van je reactie verwerkt wordt.

%d bloggers liken dit: