Quick Note: MiniKube op Windows 10

Soms heb je van die problemen waar je een dag mee bezig kunt zijn, en dan ineens heb je het opgelost. Heerlijk. Dat gevoel is waar we het voor doen, niet waar? Maar… je weet dat je volgende week hetzelfde probleem zult hebben en je dan waarschijnlijk bent vergeten hoe je het opgelost hebt.

Dit is zo’n geval. Dus: om de toekomstige Dennis (en anderen!) te helpen schrijf ik hier maar even op wat ik gedaan heb. Grote kans dus dat in de toekomst mijn probleem ga Googlen en dan hier terecht kom.

Het probleem

MiniKube is een kant en klare Virtual Machine met daarin Kubernetes en Docker volledig geïnstalleerd. Ideaal voor development: je kunt heel eenvoudig een development omgeving opzetten met daarin je Kubernetes cluster. Mits het werkt.

Ik ben een dag bezig geweest met het aan de praat te krijgen van mijn MiniKube installatie op Windows 10.

Ik had alles goed staan: ik draai Windows 10 Pro met HyperV geinstalleerd, ik had Powershell als Administrator open staan en ik had alles goed geconfigureerd. Maar wat ik ook deed: de vm wilde niet opstarten.

Ik kreeg een vage foutmelding: “Cannot start VM minikube”. Ja, dat zag ik zelf ook wel. In de HyperV manager zag ik minikube wel verschijnen maar hij verdween ook weer na een minuut of 2.

De debug output (je kent de optie wel: minikube start alsologtostderr) hielp me op weg:

I0227 08:11:40.964496   32928 main.go:110] libmachine: [stderr =====>] :
I0227 08:11:40.965503   32928 main.go:110] libmachine: [executing ==>] : C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -NonInteractive (( Hyper-V\Get-VM minikube ).networkadapters[0]).ipaddresses[0]
I0227 08:11:42.041792   32928 main.go:110] libmachine: [stdout =====>] :
I0227 08:11:42.041792   32928 main.go:110] libmachine: [stderr =====>] :
I0227 08:11:43.044237   32928 main.go:110] libmachine: [executing ==>] : C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -NonInteractive ( Hyper-V\Get-VM minikube ).state
I0227 08:11:43.797232   32928 main.go:110] libmachine: [stdout =====>] : Running

En dit zag ik tientallen keren. Als ik in Powershell het volgende commando uitvoerde, gebeurde er iets raars:

(( Hyper-V\Get-VM minikube ).networkadapters[0]).ipaddresses[0]

De output was leeg. Dus ik had geen ip adres voor mijn machine.

Na lang zoeken vond ik de oorzaak: mijn virtual switch (zowel de standaard aangemaakte als eentje die ik zelf maakte) werd verbonden met de default adapter. En in mijn geval is dat een VPN adapter….

De oplossing

De oplossing was uiteindelijk simpel: maak een virtual switch aan en zorg ervoor dat die verbonden is met de echte netwerk adapter.

Virtual Switch settings

En dat lost het probleem op. Ik ben nu gewoon verbonden met mijn netwerk. Ok, niet via de VPN maar mijn cluster kan toch niet van buiten benaderd worden en blijft ook binnen mijn eigen netwerk, dus ik zie daar niet zo’n probleem in.

Het zal wel een bug zijn…

Nog even geduld…

Ik weet het. Ik had beloofd een aantal posts te schrijven over Azure API Management. En dat komt er ook aan. Maar… Er kwam iets tussen. Iets wat de boel vertraagd. Behoorlijk vertraagd

Ik geloof in het leren door lezen. Maar: lezen op zich is niet genoeg. Ik denk echt dat je leert door te doen. Ik spreek uit ervaring: heel vaak zit ik te luisteren naar een spreker of lees ik een goed artikel over een techniek die me interessant lijkt, en ik denk het allemaal te snappen. Tot ik zelf achter de PC ga zitten en ontdek dat ik toch wat info mis, veel vergeten ben of blijkbaar halverwege het verhaal in slaap ben gevallen zodat ik overzicht kwijt ben.

Dus ik probeer zo veel mogelijk voorbeelden te geven in mijn posts waarmee je zelf aan de gang kunt gaan. Dit is iets wat ik al jaren doe, maar ik heb een tijd geleden besloten dit voortaan altijd te doen. Ik publiceer niets wat je niet zelf na kunt doen. Ik sta hier niet alleen in: mijn keuze om dit te doen is bevestigd door de blogs van mijn goede vriend Joost van Schaik: al zijn blogs zijn zo geschreven dat je het zelf kunt nabouwen (en hij levert alle code er bij ook…)

Kink in de kabel

Dus ik was druk bezig met mijn sample code voor API Management. Maar daarvoor moet je wat APIs hebben. Die had ik al beschreven in mijn vorige reeks over Azure Durable Functions dus die kon ik wel beschrijven. Alleen: ik wilde meer aspecten van APIM laten zien dan dat voorbeeld me toestond. Ik wilde schrijven over security, segmentatie in producten, audiences en nog veel meer. Mijn voorbeeld van Durable Functions is daar niet echt geschikt voor.

Dus ik begon een nieuwe sample te maken. Eentje die simpel is, begrijpelijk maar die me wel in staat stelt om APIM goed uit te leggen. En voor ik het wist was ik bezig met een heel mooi klein voorbeeld van Micro Services te schrijven.

Micro Services zijn een geweldige manier om je applicatie te ontwerpen. Een onderdeel daarvan is je services beschikbaar te stellen door middel van APIM. Maar APIM is maar een kleiner onderdeel van het verhaal.

Een ingeving

En dat bracht me op het volgende idee. Waarom schrijf ik niet een heel boek over het bouwen van Micro Services in de praktijk met Azure? En dan niet als boek maar als reeks posts.. Iedere paragraaf van het boek is een blogpost, die je achter elkaar kunt lezen maar je kunt ook hoofdstukken (reeksen posts) los lezen om te begrijpen hoe of wat.

En dat is precies wat ik nu aan het doen ben. Hoofdstuk 1 “Setting the scene” is al behoorlijk ver gevorderd, dus de eerste posts komen gauw.

Ik ga het publiceren in volgorde van schrijven, met als gevolg dat ik er later wel achter zal gaan komen dat ik toch iets moet veranderen. Nou ja, dat moet dan maar. Het editen en redigeren zal ik dan ook in real time doen zodat jullie mee kunnen lezen hoe het schrijven van een boek in zijn werk gaat.

En wat gaat me dat kosten?

Nog een voordeel voor jullie: dit boek is gratis… En ja, ik ben van plan om als alle posts er staan er een echt boek van te maken. eBook is gratis, papieren versie kost iets meer…

Dus: nog even geduld. Ze komen er aan!

Vendor Lock-in: is dat een issue?

Nee.

Zo. Dat was mijn kortste blogpost ooit. Wellicht iets te kort zelfs. En een beetje te kort: zoals alles in het leven hangt het antwoord af van de context.

Mijn professionele carrière begon zo’n 3 decennia geleden. In die tijd heb ik de industrie enorm zien veranderen. Veel van de dingen waar we ons in mijn begin tijd druk om maakten zijn niet relevant meer en vervangen door andere onderwerpen. Deze zijn uiteraard over een paar jaar ook niet relevant meer.

Maar een onderwerp blijft iedere keer terug komen: Vendor Lock In. De angst om door bepaalde keuzes te maken vast te zitten aan een leverancier. Of vast: het kan veel tijd en geld kosten om te wisselen van leverancier.

Dit is iets wat je op veel plekken terugziet, niet alleen in de IT. Op de meest onverwachte plekken kom je dit wel tegen. Een voorbeeld: mijn vrouw had een mooie pen gekregen. Je kent het wel: zo’n veel te dure pen die je vervolgens niet durft te gebruiken omdat iemand anders hem even “leent”.

Mont Blanc pen

Wat blijkt nu: als je deze wilt vullen, kan er alleen een (dure!) Mont Blanc vulling in. Je geeft dus een hoop geld uit aan een pen, en vervolgens blijf je tot in de lengte van dagen betalen aan Mont Blanc voor de vullingen.

Andere voorbeelden van wat subtielere vormen van vendor lock in zijn de kortingskaarten, spaarsystemen, membership awards en andere zaken die er maar voor zorgen dat je terugkomt bij dezelfde aanbieder. Deze zijn minder ernstig: je hebt nog altijd de keuze om ergens anders heen te gaan, hoewel je dan wat voordelen mis loopt. Bij echte lock in zit je vast een de leverancier, tot je bereid bent om een hoop geld uit te geven om te veranderen. Voor de leverancier is dat natuurlijk prachtig: ze zijn min of meer verzekerd van je klandizie.

Maar wat als deze leverancier stopt met deze dienst? Of erger nog: wat als de leverancier failliet gaat? Wat moet je dan? Dat is voor veel organisaties de reden om bij alles heel voorzichtig te zijn met de keuze van een leverancier.

Reality check

Maar is dat wel een probleem? Hoe erg is het als je met je software keuzes, platform keuzes of tooling keuzes vastzit aan een leverancier?

Het antwoord is natuurlijk: dat ligt er aan. Als je als organisatie volledig afhankelijk ben van de oplossingen die “het bedrijfje van het neefje van de baas” levert, dan heb je wel een probleem. Dit soort oplossingen zijn niet echt toekomst bestendig. Maar als je kiest voor een grote leverancier als partner, dan wegen de voordelen vaak enorm op tegen de eventuele nadelen.

Zoals met alles in ons vak is het verstandig om een risico analyse te houden. Op die manier kunnen we die risico’s kwantificeren en kunnen we een keuze maken op basis van cijfers in plaats van onderbuik gevoelens.

Toen ik begon in mijn eerste baan, hadden we een behoorlijk divers IT landschap. Onze PC’s waren verbonden door een Novell netwerk, onze email server was een Solaris machine (die bij beheer ergens in een kast stond….), onze PC’s draaiden voornamelijk Windows 3.0 en sommigen hadden een Unix machine. De database was natuurlijk Oracle. We hadden van alles wat.

In de loop van de tijd gingen we standaardiseren. We kozen om volledig voor het Microsoft platform te gaan. We installeerden Windows 3.11 For Workgroups op de PC’s, we kregen nieuwe servers met Windows NT 3.5, we kregen een ethernet netwerk, we kozen voor SQL Server als database platform. Het enige waar we afweken was de keuze van onze development tools: we kozen Borland C++ als platform om op te ontwikkelen.

En uiteraard waren er mensen die riepen dat dit heel dom was. Onze vorige oplossing was veel diverser en we hadden de risico’s gespreid. Door all-in te gaan met Microsoft hingen we de toekomst van het bedrijf op aan de toekomst van Microsoft. Ik zal de uitkomst vast verklappen: Microsoft is op het moment van schrijven het op-een-na grootste bedrijf ter wereld, mijn werkgever bestaat al dik 25 jaar niet meer…

Een jaar of 10 later kwam ik in een zelfde soort discussie. Een klant ging helemaal voor Java. Want: dat was platform onafhankelijk. Ze konden alle kanten op. De database was Oracle, maar wel via een ODBC driver (voor degene die dat niet kennen: een abstractie laag op de database zodat je onafhankelijk van de database engine dezelfde queries kon draaien).

Java logo

Iedereen vond het een geweldige oplossing. Het maakte niet uit wat de toekomst ons zou brengen: wij waren er klaar voor. Java beloofde ons een Write Once, Run Everywhere ervaring en ODBC zorgde ervoor dat we direct konden wisselen van database engine.

Dat hebben we ongeveer 2 maanden volgehouden.

Al gauw merkten we dat Java in zijn pure vorm een hele mooie programmeertaal is, maar dat de taal op zich niet belangrijk is. Het gaat om de frameworks en libraries die je gebruiken gaat.

Dus wat deden we: we importeerden de QT library, maar dan wel de variant voor Windows. Want: die ondersteunde alle GUI elementen die we nodig hadden.

ODBC ging er ook uit. Niet omdat veel ontwikkelaars ODBC al snel SlowDBC noemden (en terecht…) maar omdat we met die tussenlaag niet goed alle features konden gebruiken die Oracle ons bood. Een directe verbinding met Oracle gaf ons de mogelijkheid om onze performance te verbeteren door snellere connectivity en door gebruik te maken van typische Oracle functies.

En zo gingen we langzaam van een open, vendor-neutrale optie over naar een systeem dat met handen en voeten gebonden was aan bepaalde leveranciers.

Microsoft en .net

Een paar jaar later werkte ik fulltime met het framework van Microsoft: .net. En ook hier kreeg ik weer de vraag: moeten we hier mee verder gaan? We zijn met handen en voeten aan Microsoft gebonden.

.net logo

En ook hier gebeurde hetzelfde: hoewel .Net veel opties biedt om relatief onafhankelijk te werken (niet zo sterk als bij Java, maar toch) kozen we al gauw voor standaardisatie voor het platform, de database, de middleware, de communicatie en alle andere aspecten. .NET biedt weliswaar de mogelijkheid om bijvoorbeeld met ODBC een koppeling met de database te leggen, maar dat heeft absoluut nadelen.

Laten we eerlijk zijn: hoe vaak wisselt jouw organisatie van database leverancier? Hoe belangrijk is het om de systemen vandaag op Oracle te draaien en morgen naar SQL Server te moeten gaan? De meeste bedrijven (lees: alle) draaien al jaren op hetzelfde platform. Die willen niet over. Dus waarom zou je je in allerlei bochten wringen om die switch mogelijk te maken?

Microsoft Azure

Vandaag de dag krijg ik nog steeds exact dezelfde vragen van mijn klanten. Moeten we kiezen voor AWS, Google Cloud of voor Azure? Of leggen we ons dan vast aan een leverancier?

Mijn voorkeur mag duidelijk zijn. Ik werk al 30 jaar met Microsoft technologie (ik heb Borland C++ al heel lang geleden gedag gezegd) en dat is de omgeving die ik ken en waar ik mijn weg in kan vinden. Maar heb ik dan iets tegen AWS? Raad ik Google Cloud af? Nee, natuurlijk niet. Azure en AWS ontlopen elkaar niet zo heel veel. De ene is beter in de ene feature, de ander in de ander. Google Cloud loopt wel iets achter maar kan heel goed passen bij jouw eisen en wensen.

Ik zeg mijn klanten altijd: maak een wensen lijstje en kies daar de juiste provider bij. En als je wensen niet helemaal passen bij wat die aanbieder heeft, pas dan als het mogelijk is je wensen aan. Cloud aanbiedingen veranderen continue en wellicht is een andere oplossing beter dan wat je eerst dacht. Maar: wees niet bang voor Vendor Lock in van een bedrijf als Amazon, Google of Microsoft. Deze bedrijven blijven voorlopig wel actief. Microsoft Azure is enorm open en als je graag alles op Linux wilt, dan kan dat (welke Linux? Let op: ook hier is lock-in een risico). Vendor Lock-in is een term die veel mensen gebruiken maar de risico’s hier zijn minimaal ten opzichte van de winst als je kiest voor standaardisatie.

Nou ja, behalve als je gaat voor het neefje van de baas. Die optie is iets wat je wellicht goed moet heroverwegen…

Feedback: de vergeten kant van DevOps

Schoolbord met het woord Feedback

DevOps. Developers and Operations. Iedereen in ons vakgebied heeft het er over. En helaas: slechts weinig organisaties doen het goed. Dat is jammer: DevOps is geen menu systeem waar je uit kunt halen wat je leuk vindt en de rest kunt negeren. Je haalt het meeste rendement uit DevOps als je alle stappen doet en gebruikt.

“Walk tall, or don’t walk at all.”

In het nummer New York City Serenade van Bruce Springsteens “The Wild, The Innocent and the E-Street Shuffle” uit 1973 zegt de hoofdpersoon op een gegeven moment: “walk tall, or don’t walk at all.” En dat is precies wat ik hier terug zie. Doe het goed, of doe het niet. Haal je niet de ellende op de hals van het omgooien van je huidige organisatie rondom je development teams als je toch de benefits er niet uit kunt halen. Waarom zou je?

Veel bedrijven kijken naar DevOps en zien eigenlijk maar een stuk: de pipeline. Developers maken iets en auto-magisch verschijnt dat in productie. We worden enthousiast gemaakt met verhalen over Amazon die meer dan 7000 deployments per dag zouden doen en denken: “Dat wil ik ook!”

Maar als je niet het hele proces mee neemt kun je net zo goed niets doen. De enige reden dat organisaties zo succesvol zijn met DevOps is omdat ze alle aspecten goed overwogen hebben en meegenomen hebben in hun planning.

Cargo Cult Syndroom

Ok. Vraagje: wie herkent dit? De organisatie roept vol trots dat we vanaf vandaag Agile werken. Want: we hebben daily stand ups!

Dat is niet agile. Dat is wat we zo mooi het Cargo Cult Syndroom noemen. Cargo Cult is al langere tijd bekend onder antropologen, de oudste beschrijving ervan stamt uit 1885. Maar het meeste bekend is wel het effect hiervan op de bevolking van kleine eilanden in Guinea na de tweede wereldoorlog. De Amerikanen, op weg naar Japan, gebruikte die eilanden als uitvalsbasis. De lokale bevolking zag deze grote, voornamelijk blanke Amerikanen als goden: ze kwamen met vliegtuigen en hadden chocola, sigaretten en andere wonderlijke dingen bij zich. Toen de Amerikanen de eilanden verlieten wilden de bewoners ze terug halen. Dus gingen ze herhalen wat ze de Amerikanen zagen doen in de hoop dat dat deze goden terug zouden lokken. Dus probeerden ze de cultuur van deze Amerikanen na te bootsen. Inclusief het nabouwen van vliegtuigen, verkeerstorens, zendapparatuur en dergelijke.

Cargo Cult

Uiteraard hadden ze geen idee waarom ze dit deden. De Amerikanen keken bijvoorbeeld door verrekijkers, maar dat werd door de plaatselijke bevolking uitgelegd als de manier waarop je hoorde rond te kijken als je de vliegtuigen naar je toe wilde lokken.

Het meest verbazende is dat deze mensen dit geloof heel lang hebben volgehouden. Decennia lang hebben de lokale mensen vliegtuigen nagebouwd, in de hoop de goden terug te lokken.

Het is makkelijk om deze mensen af te doen als onderontwikkeld. Sommige lachen er om. Maar we doen eigenlijk allemaal hetzelfde.

Een Daily Standup is een gebruik dat in het leven geroepen is met een reden. Dit doen we om een paar dingen te bereiken:

  1. Update van de status, zodat iedereen in het team weet waar iedereen mee bezig is
  2. Herkennen van problemen, zoals het te lang blijven vast zitten in een taak
  3. Mogelijkheid tot het bieden van hulp: door te bespreken waar er aan gewerkt wordt kunnen mensen hun expertise aanbieden indien nodig
  4. Versterken van het team gevoel, van samen aan een product te werken

Als je echter maar gewoon een dagelijks praatje met elkaar gaat houden, zonder het bovenstaande in het achterhoofd te houden, dan doe je aan Cargo Cult. Je doet wat je gezien hebt zonder de achterliggende boodschap te begrijpen. Het resultaat: hetzelfde als op Melanesie: niets.

Feedback

Dit plaatje kennen we allemaal wel, denk ik:

DevOps cycles

De meeste organisaties volgen dit wel aardig goed. Zeker het eerste stuk links is meestal goed geregeld. Dat is op zich niet zo vreemd: de meeste DevOps projecten worden gestart vanuit de Development kant.

De taken Plan, Create, Verify en Package zijn niets nieuws voor de meeste ontwikkelaars. Release, Configure en Monitor is iets wat Operations altijd al doet. DevOps zorgt er voor dat deze twee teams samen komen en de verantwoordelijkheid delen waardoor er, als het goed is, synergie ontstaat. Ik geef toe: dit is een enorm overgesimplificeerde weergave van een complex geheel, maar de basis klopt wel.

Maar.. En dit is de grote maar… Er loopt een pijl van Monitor naar Plan (eigenlijk is dat al Plan). En die wordt vaker wel dan niet vergeten. Er is heel vaak geen terugkoppeling van Monitor naar Plan.

Ok. Bugs worden gerapporteerd en opgelost. Maar dat is niets nieuws. Er vindt over het algemeen geen terugkoppeling plaats van de dagelijkse monitoring. Alleen als er problemen zijn, wordt er aan de bel getrokken. Met andere woorden: in plaats van een circulair proces is DevOps vaak een lineair proces. Het begint bij de plan en eindigt als het product gereleased is. Het is een project in plaats van een proces.

Het hele idee van DevOps is nu net juist de goede competenties bij elkaar te krijgen en daar van te leren. In plaats daarvan doen we hetzelfde als we altijd al deden, alleen noemen we het anders. We roepen dat we DevOps doen, we hebben gezamenlijke stand-ups en we doen niets anders dan de bewoners van Melanesie na de tweede wereldoorlog: Cargo Cult.

Feedback op alle niveaus

Wat mij dwars zit aan het plaatje hierboven, is dat het een model is van de werkelijkheid maar dat mensen denken dat het model de werkelijkheid is. Een model is heel handig als je iets wilt versimpelen en daar over wilt praten. Maar je moet nooit vergeten dat datgene wat het model voorstelt veel ingewikkelder en uitgebreider is dan het model. Het plaatje is een model van het werkelijke proces, waarbij details weggelaten zijn. Dat moet ook wel, want anders wordt het plaatje een chaos.

Iedere stap in het proces bevat feedback momenten. Een aantal kennen we wel en hebben we een naam gegeven. Plan bijvoorbeeld wordt in Scrum ondersteund door feedback vanuit de Backlog refinement en de planning sessies. Create krijgt feedback vanuit de daily standups. We hebben de sprint-review (als je Kanban doet, moet je nog steeds deze review sessie houden!), we hebben de retrospective enzovoorts. Maar hoe zit het met de feedback tussen de stappen in? Iedere stap in het proces is een moment om te leren en te verbeteren. Ik denk dat het een slecht idee is om deze stappen als discrete, op zich staande stappen te zien. Het is meer een gradient waarin de ene fase overloopt in de andere met veel momenten en mogelijkheden tot feedback. Dat laatste gebeurt helaas zelden. Het gevolg? Veel fouten worden herhaald. Veel stappen die beter gedaan worden, worden niet verbeterd omdat die feedback niet bij de juiste personen terecht komt.

Er is vaak sprake van kleine groepjes die een van de stappen neemt die in het model staan. Dat zijn eilandjes op zich. Daarna wordt het resultaat hiervan over de muur gegooid en de volgende mag het oppakken.

De lange-termijn effecten zijn nog erger: op een gegeven moment zie je vaak dat er toch weer twee kampen ontstaan: developers versus operations. En dan zijn we weer terug waar we waren. Meestal is het trouwens het management dat nog lang volhoudt dat we toch echt DevOps doen.

Weet je wat je doet en waarom

De principes van DevOps zijn niet zo ingewikkeld. Het goed en consequent uitvoeren daarvan wel. Weet wat de principes zijn, maar vooral: weet waarom die principes er zijn. Snap wat de achterliggende gedachten zijn. Snap wat de hordes zijn die we moeten overwinnen en hoe de stappen binnen DevOps deze helpen overwinnen.

Wees geen Cargo Cult aanhanger. Wees pro-actief, leer bij, snap en vooral: geef continue feedback op alles en iedereen in het proces. Leer van het proces, koppel terug en dan pas zal DevOps gaan werken.

Azure Durable Functions, deel 11: App LifeCycle, deel B

We gaan verder waar we gebleven waren in het laatste deel: we hadden App Insights toegevoegd en we kunnen nu heel mooi zien wat er allemaal gebeurt. En alles is mooi en goed in onze wereld. Tot het onvermijdelijke gebeurt…. En dat is vaak een van de volgende dingen:

  1. Er is een bug gevonden in onze code
  2. De requirements veranderen

Wij doen niets fout, dus punt 1 is niet zo waarschijnlijk, niet waar? Nou ja, ok, ook wij maken fouten dus het kan gebeuren. Punt 2 komt uiteraard ook heel vaak voor. In ieder geval, we moeten een change doorvoeren…

API Versioning

We kunnen een aantal soorten changes herkennen in Durable Functions App.

  1. Changes in Activities die geen andere signature krijgen in de functions (dus bool ValidateInput(string email) blijft bool ValidateInput(string email)
  2. Changes in Activities die een andere signature veroorzaken (dus bool ValidateInput(string email) wordt ValidationResult ValideInput(Attendee attendee)
  3. Changes in de Orchestration.

Mogelijkheid 1 is het meest makkelijk: maak je changes, doe alle testen, en deploy de code (of eigenlijk: check in de code in master op je repository en zorg er voor dat je pipelines de deployment regelen) (nu ik de vorige zin lees, vraag ik me af hoeveel van mijn niet-IT vrienden überhaupt snappen wat ik in die zin zeg…)

Veranderingen in de orchestration code echter zijn wat lastiger. Dit kan zijn dat er een verandering in de orchestration zelf komt of dat de activities een andere signature krijgen. Dat laatste houdt natuurlijk in dat de resultaten in de TableStorage er opeens anders uit gaan zien.

Op zich is het allemaal geen probleem: we kunnen deployen waar en wanneer we maar willen. Maar wat als er op het moment van deployen net een workflow loopt? Wat gebeurt er dan? Stel je de volgende flow voor:

Main orchestration flow

Deze flow kennen we. Ik heb hem even versimpeld: de resultaten van Validate Input en Notify Board zijn weggelaten. Maar dit is de basic flow zoals we die nu de hele tijd gebruiken.

We hebben dit op Azure staan en iemand meldt zich aan op de website. De flow begint te draaien. De input wordt gevalideerd, deze wordt goed bevonden. De orchestration begint opnieuw, haalt het resultaat op van de ValidateInput en gaat verder. De 4 usergroups worden geinformeerd. Ook hier weer begint daarna de flow opnieuw, de resultaten worden opgehaald en we gaan naar stap 3: Notify Board. Maar.. vlak voordat de flow opnieuw begint is er een nieuwe versie van NotifyGroups uitgerold. Dit keer doen we niet alleen een notificatie, maar krijgen we feedback terug vanuit de API’s van de andere groepen. Het resultaat is een OK of een ToManyNoShows als iemand te vaak niet komt zonder af te melden. Dat willen we graag weten: we kopen immers eten en drinken voor die persoon en als die niet komt dan hebben we een probleem. Die info willen we graag meegeven aan NotifyBoard.

Maar ja, de flow loopt al. De huidige attendee is door de NotifyGroups heen gekomen zonder dat daar een advies bij kwam kijken. Dus we hebben geen waardes om mee te geven aan Notify Board. Wat moeten we nu doen?

Er zijn een paar oplossingen.

  1. Doe een deployment. Alle lopende workflows krijgen een error en worden afgebroken. Niet zo heel netjes maar in development doen we niet anders.
  2. Geef de nieuwe versie een andere orchestration naam en roep die aan in plaats van de oude. De lopende flows zullen onder de oude orchestration door gaan, de nieuwe gaan naar de nieuwe flow toe. Op zich een goede oplossing, maar je krijgt wel veel verschillende Function Apps in Azure die eigenlijk niet meer ter zake doen. Een redelijke oplossing, maar niet zo mooi als:
  3. Maak een nieuwe hub aan. De huidige flows blijven lopen tot ze klaar zijn. De nieuwe requests gaan via de nieuwe flow. De oude dll’s worden gecached tot ze niet meer nodig zijn en gaan dan weg. De oude data blijft bewaard, apart van de nieuwe.

Die laatste optie klinkt als de beste optie. Dus laten we dat gaan doen.

En dat is veel eenvoudiger dan je zou verwachten.

In onze solution hebben we een file genaamd host.json. Deze is redelijk leeg:

{
  "version": "2.0"
}

Verander deze tot hij er zo uitziet:

{
  "version": "2.0",
  "extensions": {
    "durableTask": {
      "hubName": "meetingReg"
    }
  }
}

De hub is de naam van de flow waarin we draaien. Dat is standaard DurableFunctionsHub. Die naam zien we dan ook steeds terugkomen in de Storage Explorer:

Storage Explorer met hub naam

Als we echter de host.json aanpassen zoals hij nu hierboven staat (dus met als hubnaam meetingReg) en we draaien de flow nog een keer, zien we dit:

Storage Explorer met extended hubnames

Je ziet dat er een paar tabellen bijgekomen zijn. Hetzelfde geldt voor de queues: alle queues die we hadden zijn er nog maar er 5 bijgekomen: 4 control queues en een workitems queue, maar nu met de prefix meetingReg.

Als we dit zo zouden deployen naar Azure, zullen de lopende flows blijven lopen op een gecachte versie van de assembly en de data staat in de ‘oude’ tabellen en queues. Met andere woorden: de lopende flows blijven doorlopen. Niets aan de hand.

Alle nieuwe requests echter gaan naar de flow met de nieuwe hub naam. Die krijgen de nieuwe assemblies en hun data wordt opgeslagen in de nieuwe tabellen en queues.

Het enige wat je om de zoveel tijd zou kunnen doen, is het opruimen van de oude data. Uiteraard is het voor veel bedrijven belangrijk (of zelfs verplicht) om data te bewaren dus dan moet je de tabellen niet weggooien. Maar je hebt in ieder geval de keuze.

Dit is een heel verhaal geworden, maar in feite komt het neer op het volgende:

Als je iets verandert in een orchestration function (volgorde of de manier van aanroepen van de activities), rol dan een nieuwe versie uit met een andere hub naam in de host.json.

That’s it.

Security

Wellicht het belangrijkste onderwerp van deze hele reeks is de veiligheid. En daar ga ik het allerminst over zeggen. Maar het is wel de afsluiting van deze reeks.

Durable Functions zijn secure by default. Alle data staat in een Storage Account die je, als het goed is, niet van buiten af kunt benaderen. De orchestration en activity functions kunnen alleen aangeroepen worden vanuit de Azure omgeving. Niemand anders kan daar bij. De enige functions die dat kunnen doen zijn de standaard Azure Functions, zoals onze StartRegistration, ApproveAttendee (vanuit de email) en StartDatabaseCleanup (die de eternal workflow opstart).

Deze functions moet je wel beveiligen. Zoals we gezien hebben, kun je deze op 3 manieren dichtkrijgen.

  1. Geen beveiliging, anonymous access is toegestaan
  2. Met key op Function App niveau (admin type security): alles binnen de function app werkt met dezelfde key
  3. Met key op function niveau: iedere function krijgt een eigen key.

Die key wordt voor je gegenereerd maar die kun je in de Azure Portal weggooien en nieuwe genereren. Ook kun je meerdere keys genereren voor dezelfde functie. Op die manier kun je iedere gebruiker een eigen key geven (en dus per gebruiker, klant of groep ook weer revoken).

In je source code heb je al aangegeven dat je de functie op functie niveau wilt beveiligen:

[FunctionName("StartRegistration")]
public static async Task<HttpResponseMessage> StartRegistration(
    [HttpTrigger(
        AuthorizationLevel.Function,
        "get",
        Route = "dotNed/register/{name}/{email}/{wantsTweet}")]
    HttpRequestMessage req,
    [OrchestrationClient] DurableOrchestrationClient client,
    string name,
    string email,
    bool wantsTweet,
    ILogger log)
{

Je ziet op regel 4 dat we daar zeggen dat we de authorization level op Function willen.

In de Azure Portal ga je naar je Function App en vind je function daar. In de Manage tab van die functie vind je dan de keys en alle beheermogelijkheden daarvan:

Function key management in de Azure Portal

Hier kun je keys maken, renewen en revoken.

Meer security

Is dit genoeg om je hele systeem te beveiligen?

Nee.

Maar het is een begin. Om de boel echt goed op slot te krijgen kunnen we beter gebruik maken van API Management. Daarmee kunnen we versie beheer doen van onze publieke functies, we kunnen revisions aanbrengen, we kunnen rechten toekennen op niveau van Active Directory en nog veel meer.

Sterker nog: dat is dusdanig veel meer dat ik daar een nieuwe serie over ga schrijven. Dus die hou je nog even tegoed.

Afsluitend

En dat is eigenlijk alles wat ik tot op dit moment wil en/of kan delen over Azure Durable Functions. Er komt een nieuwe versie aan met een hele hoop breaking changes maar die is nog niet beschikbaar. Althans: niet in een stabiele versie. Als die uitkomt zal ik daar uiteraard een hele hoop aandacht aan besteden. Een van de dingen die daar in komen is bijvoorbeeld Durable Entities. Met andere woorden: we hebben zo meteen een instance van onze Attendee class en die ‘zweeft’ ergens rond in Azure. Die kunnen we aanroepen en dingen mee laten doen. Maar die is er gewoon…

We hebben een hoop bekeken en een hoop gedaan. Ik hoop dat je er wat van geleerd hebt en dat je er je voordeel mee kunt doen. Maar vooral hoop ik dat je Durable Functions gaat toepassen in je dagelijkse werk.

Veel plezier!

Azure Durable Functions, deel 11: App Lifecycle, deel A

We zijn klaar met spelen. Het is tijd voor het echte werk. Tot nu toe hebben we alles lokaal gedaan met de Storage Emulator, maar nu is het tijd om te deployen naar Azure. We gaan de Cloud in! En daar lopen we ook tegen een paar dingen aan:

  1. Beheer
  2. Monitoring
  3. Versioning bij veranderingen (deel B)
  4. Security (deel B)

We zullen ze allemaal bekijken!

Deployment

Vanuit Visual Studio is het deployen van je Function App een fluitje van een cent. Je moet wel een Azure subscription hebben (een trial is gratis…). Verder raad ik je aan om alvast een Resource Group aan te maken in de portal. Dan heb je die maar vast.

Deployen gaat zoals altijd gewoon vanuit Visual Studio met de rechter button op het project en dan Publish. Uiteraard kan dit beter, maar we zullen naar CI / CD kijken als het moment daar is. Ik raad sowieso aan om eerst een handmatige deployment te doen.

In VS kunnen we dus Publish kiezen. Dan krijg je deze dialog:

Azure Deployment start dialoog.

Je hebt verschillende soorten van deployment. Ik raad in dit geval aan om Azure Functions Consumption plan te kiezen. Premium is sneller maar je betaalt een heel stuk meer. Bij Consumption betaal je naar gebruik. Dus als je workflow wacht op de beheerders tot ze eindelijk een keer die link in de email openen, betaal je niets. We blijven Hollanders, niet waar?

Klik op Create Profile.

Publish new App Service dialoog

Ik heb even wat waardes aangepast (naam van de service, location en zo) maar echt spannend is dit allemaal niet.

Click Create. Dit zorgt er niet voor de deployment plaats vindt, maar dat er een profile aangemaakt wordt waar vanuit je kunt gaan deployen. Er is dus nog niets naar Azure toe.

Laten we dat eens veranderen.

Publishing dialoog.

Dit scherm verschijnt. Nu kunnen we gaan deployen maar ik raad je aan om eerst op de link voor “Edit Azure App Service settings” te klikken:

App Settings voor de functie.

Je kunt dit namelijk ook prima achteraf in de Azure Portal doen, maar nu krijg je ook te zien hoe je development settings waren.

Je ziet de settings voor de Usergroups en die voor SendGridAPIKey. Die kun je gewoon kopiëren. De AzureWebJobsStorage is lokaal ingesteld op de emulator, maar remote staat hij ergens anders op (je ziet maar een klein stukje van mijn AccountKey, dus die hoef ik niet zwart te maken). De rest laten we lekker zoals het is.

Klik OK. Klik Publish. Haal koffie…

Het duurt even maar op een gegeven moment is hij wel klaar. Onderin je output window staat iets als Publish: 1 succeeded.

Het wordt tijd om de Portal te bekijken. Ga naar de https://portal.azure.com, vind je Resource Group en kijk wat er allemaal gebeurt is. Bij mij ziet er nu zo uit:

Overview deployed items in de Portal

Er zijn 3 resources aangemaakt.

  1. meetingregistration: een Storage Account. Deze bevat de queues, TableStorage en meer. Die had ik al handmatig aangemaakt dus deze telt niet
  2. MeetingRegistrationAppService: de App Service die de binaries bevat.
  3. WestEuropePlan. Mijn App Service plan . Niet zo spannend.

Klik op de App Service om te kijken wat we daar hebben:

Deployed functies in de Azure Portal

Als het goed is, zie je nu waarom ik met de O_ en A_ prefix werk. Zelfs in deze hele kleine flow hebben we al best wat functies. We hebben twee publieke functies (de starter en de cleanup), we hebben 3 orchestrators en we hebben 5 activities.

Kies de starter functie StartRegistration. In het detail window zie je nu </> Get Function URL.. Open die en kopieer de inhoud.

Function App details

Deze URL kun je nu pasten in de browser. Vervang de standaard dummy waardes {name}, {email} en {wantsTweet} door echte waardes (om te testen geef ik hem even een ongeldige email zodat we niet door de hele flow heen hoeven).

Als ik dan op de statusQueryGetUri klik, krijg ik dit:

// 20191104161833
// https://meetingregistrationappservice.azurewebsites.net/runtime/webhooks/durabletask/instances/287094e6d11e48f8b7b73904c6ea206a?taskHub=DurableFunctionsHub&connection=Storage&code=4CtGP4ZKJJZYQ85RyhZ1PrILDm6zG3quHWOe9pkVtrOq26eCroxNsw==

{
  "name": "O_PerformRegistration",
  "instanceId": "287094e6d11e48f8b7b73904c6ea206a",
  "runtimeStatus": "Completed",
  "input": {
    "$type": "MeetingRegistration.Attendee, MeetingRegistration",
    "Name": "Dennis",
    "Email": "dennisatvroegop.org",
    "WantsTweet": true
  },
  "customStatus": null,
  "output": {
    "IsSucces": false,
    "Reason": "Not a valid email given."
  },
  "createdTime": "2019-11-04T15:18:29Z",
  "lastUpdatedTime": "2019-11-04T15:18:30Z"
}

Alles is goed gegaan. Nou ja, technisch dan. Het email adres was niet geldig, dus die rejecten we. Als je dit nu helemaal gaat uitvoeren, dus met geldig email adres, let er dan even op dat we in onze code hard-coded HTTP gebruiken in de link in de email. Dat moet hier uiteraard HTTPS zijn….

Monitoring

Als je wilt weten hoe het met je functies gaat, is Application Insights de beste methode. Om dat te doen, moet je in je Resource Group een AppInsights instance aanmaken:

Aanmaken van een App Insights instance

Als de App Insights resource gemaakt is, kun je er naar toe navigeren. In het menu links kun je dan kiezen voor API Access.

App Insights details.

Hier kun je dan een key genereren. Kies alle opties, voor nu is dat prima. Je krijgt dan een key die je moet kopieren en plaatsen bij de App Service. Dat gaat heel eenvoudig: navigeer naar je App Service en zie bovenin staan “Application Insights is not fconfigured”

Overview App Service met App Insights

Klik daarop en volg de wizard. Het enige wat dit doet, is in je settings van je app service een key toevoegen met de naam APPINSIGHTS_INSTRUMENTATIONKEY met de key die we net hadden aangemaakt erin.

Draai je workflow nog een keer, en ga dan naar die App Insights. Dit keer draai ik hem helemaal.

App Insights - Application Map

Dit is de map. Ik heb 1 instance, die doet 5 calls naar de Azure Table en 1 call naar de Queue. En oh ja, iets naar API.SendGrid.com

Als ik naar de Overview ga, zie ik 5 errors:

Errors in App Insights

Dat klopt natuurlijk: dat zijn de exceptions van onze ‘twitter-client’.

Ik kan daarop helemaal inzoomen:

Error details.

Je ziet: met Application Insights kun je enorm veel informatie verzamelen op een hele makkelijke manier.

En verder?

Ik had je nog een stukje beloofd over versioning en security maar dat doen we in de volgende post wel.

Succes!

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!

Azure Durable Functions, deel 9: errors en robuuste functies

Onze workflow is bijna klaar. We hebben nog een stap te gaan: het al dan niet versturen van de tweet met daarin de tekst “{attendee} komt naar onze bijeenkomst!” Deze tweet is optioneel: niet iedereen wil dat delen met de wereld.

Ik ga niet de code schrijven die daadwerkelijk naar Twitter gaat. Die voorbeelden zijn genoeg te vinden. Echter: Twitter wil wel eens fouten genereren. Je bent over je limiet heen, of Twitter is tijdelijk down. Hoe gaan we daar mee om? Laten we dat eens simuleren in de activity:

Uiteraard hebben we eerst een DTO nodig:

namespace MeetingRegistration
{
    public class TweetData
    {
        public string Name { get; set; }
    }
}

We hadden ook gewoon een string kunnen doorgeven, maar ik hou van expressieve code. Vandaar deze class.

De activity:

[FunctionName("A_SendTweet")]
public static void SendTweet([ActivityTrigger] TweetData tweetData, ILogger log)
{
    log.LogWarning($"About to send a tweet about {tweetData.Name}.");
    if (new Random().NextDouble() > 0.3)
    {
        throw new Exception("Twitter is down!");
    }
    log.LogWarning("Successfully sent out the tweet...");
}
    

Ok, ik geef toe: Twitter is wel wat robuuster dan dat. 1 in de 5 tweets lukken slechts. Maar dit geeft me wel mooi de kans om te laten zien hoe we de boel wat stabieler kunnen krijgen.

De orchestrator zouden we als volgt kunnen maken (na het wachten op het externe event):

var tweetData = new TweetData {Name = attendee.Name};
int maxCount = 5;
bool tweetIsSent = false;
while ((!tweetIsSent) && (maxCount-- > 5))
{
    try
    {
        await ctx.CallActivityAsync("A_SendTweet", tweetData);
        tweetIsSent = true;
    }
    catch (Exception ex)
    {
        log.LogError("Oeps!" + ex.Message);
        // Wait 5 seconds before retrying
        Task.Delay(5000);        
    }
}

if (!tweetIsSent)
{
    log.LogError("Tweet did not get sent after retrying.. too bad!");
}

Zo iets dus. Maar dit is niet echt handig: ten eerste is dit niet deterministisch, dus dit zou eigenlijk in een activity moeten. Daarnaast: het is weer plumbing. En daar willen we zo veel mogelijk vooraf.

Dit is hoe bovenstaande code WEL moet:

var tweetData = new TweetData {Name = attendee.Name};

var retryOptions = new RetryOptions(TimeSpan.FromSeconds(5.0), 5);
try
{
    await ctx.CallActivityWithRetryAsync("A_SendTweet", retryOptions, tweetData);
}
catch (Microsoft.Azure.WebJobs.FunctionFailedException functionFailedException)
{
    log.LogError("Tweet did not get sent after retrying.. too bad!\n" + functionFailedException.Message);
}

We beginnen weer met een TweetData instance. Hierna maken we een instance aan van een RetryOptions class. Deze heeft als constructor parameters de tijd die tussen retries in moet zitten, en een aantal keer dat het opnieuw geprobeerd mag worden. Er zijn nog meer opties, zoals de initiële tijd tussen de eerste call en de tweede, of de tijd moet toenemen tussen pogingen door en nog meer van dat soort dingen.

We roepen in een try catch CallActivityWithRetryAsync aan en geven, naast de normale parameters, nu ook de RetryOptions mee. Als er nu iets fout gaat in de activity, wordt deze, na 5 seconden, nog een keer aangeroepen. En dit tot een maximum van 5 keer. Als het dan nog niet gelukt is, krijgen we een FunctionFailedException en daar kunnen we wel wat mee.

Het zal je niet verbazen dat CallSubOrchestratorAsync ook een dergelijke variant heeft: CallSubOrchestratorWithRetryAsync. En die werkt net zo.

Ik raad je aan om deze variant zo veel mogelijk te gebruiken op plekken waar er maar iets fout zou kunnen gaan. Op deze manier is je function veel robuuster geworden.

Oh ja: ik weet dat ik NIET controleer of de parameter wantsTweet wel op true staat… Maar dat soort code laat ik graag aan jou over. Het vervuilt mijn voorbeelden zo…

En nu?

Onze workflow is klaar! We kunnen aan het werk en we kunnen inschrijvingen verwachten op onze events. Maar wij als ontwikkelaars zijn nog niet klaar (zijn we dat ooit?).

Want: onze approvalAttendee database loopt lekker vol met data die we niet meer nodig hebben. Maak je geen zorgen: dat gaan we in de volgende post oplossen!

Azure Durable Functions, deel 8: vervolg van de workflow na menselijk goedkeuren.

Onze workflow begint al aardig volwassen te lijken. We hebben de input gevalideerd, we hebben berichten verstuurd naar andere groepen en we hebben een email verstuurd en de info daarover in de database geplaatst. Dat laaste hebben we in onze vorige post gezien.

Onze workflow staat nu keurig te wachten tot het een seintje krijgt om verder te gaan (of tot er een time-out plaatsvindt). Hoe doen we dat? Simpel: met Azure Functions!

De link die we in de mail versturen is een link naar een Azure Function. Dus we gaan die maar eens maken. Het is een gewone, recht-toe, recht-aan Azure Function dus niet Durable. Hij is publiek dus hij kan in de class PublicFunctions.cs.

[FunctionName("ApproveAttendee")]
public static async Task<HttpResponseMessage> ApproveAttendee(
    [HttpTrigger(
        AuthorizationLevel.Anonymous,
        "get",
        Route = "processApproval/{rowKey}/{isApproved}")]
    HttpRequestMessage req,
    [OrchestrationClient] DurableOrchestrationClient client,
    string rowKey, bool isApproved,
    ILogger log
)
{
    log.LogWarning($"We received {isApproved} for key {rowKey}.");
    return req.CreateResponse(HttpStatusCode.OK);
}

De FunctionName heeft geen prefix. Het is immers een gewone publieke method. We zetten de authenticatie op anonymous: we willen geen authenticatie keys delen met het publiek en de unieke ID is authenticatie genoeg voor ons. Voor de rest doen we niets spannends. We zetten de route goed zodat we de parameters krijgen. Deze route past bij de route die we in de activity hebben waar we de email aanmaken.

Ik vraag ook gelijk een instance van de DurableOrchestrationClient toe: die hebben we later nodig.

Als we dit nu starten, zien we in de debug informatie van de runtime nu ineens twee publieke functions:

Runtime info me de juiste URLS

We hebben nu dus ApproveAttendee erbij. Ik weet het: de naam is niet helemaal goed want we kunnen potentiele Attendees ook afwijzen maar ik benader dingen graag positief.

Als we nu naar de mail gaan die we verzonden hebben terwijl de runtime draait, kun je op de link klikken (approve of deny, net wat je wilt). De URL is immers geldig nu. Zo gauw je dat doet, zul je het resultaat in de debug zien verschijnen.

Debug info van een geslaagde call naar de API voor het goedkeuren.

Vervolgen van de workflow

Ok. Laten we de code gaan schrijven die de workflow weer verder helpt.

Als eerste hebben we de instanceId van de workflow nodig. Die moeten we immers een signaal sturen. Die instanceId staat in de database. Om die op te halen moeten we een partitionkey en een rowkey hebben. Die hebben we ook: de partitionkey was hardcoded op AttendeeApprovals en de rowkey krijgen we mee in de call naar deze function. Door middel van de juiste injection in de signature van deze method kunnen we de data dus opvragen:

[FunctionName("ApproveAttendee")]
public static async Task<HttpResponseMessage> ApproveAttendee(
    [HttpTrigger(
        AuthorizationLevel.Anonymous,
        "get",
        Route = "processApproval/{rowKey}/{isApproved}")]
    HttpRequestMessage req,
    [OrchestrationClient] DurableOrchestrationClient client,
    [Table(
        "Attendees", 
        "AttendeeApprovals", 
        "{rowKey}", 
        Connection = "AzureWebJobsStorage")] 
    StoredOrchestration storedOrchestration,

    string rowKey, bool isApproved,
    ILogger log
)
{

De method heeft nu een Table erbij gekregen, maar in tegenstelling tot de activity, geven we hier nogal wat extra data mee. In het attribuut Table hebben we nu de Table name, de partitionkey en de rowkey (die komt uit de route) en de Connection weer. Het is in de vorm van een StoredOrchestration, want zo hadden we hem ook weggeschreven. Dit keer is het geen out-parameter want we krijgen hem nu mee.

Als iemand nu deze function aanroept, zal de runtime de juiste data ophalen en in de storedOrchestration plaatsen. Als er geen data is voor deze rowKey krijg je een NULL value terug. Daar kunnen we op controleren: als iemand maar wat verzint en intypt, hebben we geen bijbehorende entry in de tabel en dus hoeven we er niets mee.

Het enige wat ons nu nog rest is de runtime vertellen dat de flow verder mag en wat de beslissing van het bestuur is geweest. De hele method ziet er nu dus zo uit:

[FunctionName("ApproveAttendee")]
public static async Task<HttpResponseMessage> ApproveAttendee(
    [HttpTrigger(
        AuthorizationLevel.Anonymous,
        "get",
        Route = "processApproval/{rowKey}/{isApproved}")]
    HttpRequestMessage req,
    [OrchestrationClient] DurableOrchestrationClient client,
    [Table(
        "Attendees", 
        "AttendeeApprovals", 
        "{rowKey}", 
        Connection = "AzureWebJobsStorage")] 
    StoredOrchestration storedOrchestration,

    string rowKey, bool isApproved,
    ILogger log
)
{
    log.LogWarning($"We received {isApproved} for key {rowKey}.");

    if (storedOrchestration == null)
    {
        log.LogWarning("Received illegal request.");
        return req.CreateResponse(HttpStatusCode.NotFound);
    }

    await client.RaiseEventAsync(storedOrchestration.OrchestrationId, "E_BoardHasApproved", isApproved);

    return req.CreateResponse(HttpStatusCode.OK);
}

Dus: we halen de data op, we krijgen de storedOrchestration (of niet…). We controleren die data en als die null is, geven we een mooie 404 terug.

Als de data er wel is, roepen we de workflow runtime aan met de method RaiseEventAsync. Die krijgt de naam die we ook al bij WaitForExternalEventAsync hadden gebruikt: “E_BoardHasApproved” en als payload geven we de waarde true of false mee. Daarna geven we een 202 status code terug.

Als je dit nu runt, zul je zien dat zo gauw we de RaiseEventAsync aanroepen, de orchestrator gaat replayen en weer verder gaat.

En nu?

We zijn er bijna. We moeten alleen nog een tweet versturen. Dat doen we in de volgende post!

Azure Durable Functions, deel 7: Human Interaction

Zoals ik aan het einde van de vorige post al schreef, hebben veel workflow engines de mogelijkheid om het systeem te pauzeren tot er een signaal van buiten komt die aangeeft dat we verder kunnen.

Nu is Durable Functions geen echte workflow engine maar we kunnen dit wel hier ook inbouwen. Nu is het op zich niet zo ingewikkeld, maar er zitten wat haken en ogen aan. Immers: op het moment dat je de mens een onderdeel maak van een geautomatiseerd proces moet je rekening houden met het grillige gedrag van mensen. Computers zijn voorspelbaar. Mensen niet.

Het proces is dus vrij recht-toe, recht-aan. Maar we willen graag een robuuste omgeving maken die rekening houdt met alle eventualiteiten.

De proces stap die we hier behandelen is de volgende: stap 3 in het proces, genaamd “Get Approval”

Het idee is dat wij als bestuur van Stichting dotNed toch wel even kijken wie zich allemaal aanmelden zodat we sommige mensen kunnen weigeren (doen we nooit hoor…) Hoe doen we dat? Simpel: op het moment dat iemand zich aanmeldt, krijgen we een mailtje in een centrale mailbox met daarin de tekst:

“Persoon {x} heeft zich aangemeld voor de bijeenkomst. Klik {link A} om deze persoon te accepteren en klik {link B} om deze persoon te weigeren.”

Of woorden van die strekking. Door link A of link B te kiezen kunnen we onze voorkeur aangeven.

Onze worfklow zal dus een mail moeten versturen. Maar: daarna moet de flow gepauzeerd worden tot iemand op een van beide links geklikt heeft. Die link zal dan een trigger moeten hebben die de workflow weer aanzwengelt zodat we verder kunnen gaan met het accepteren of afwijzen.

Nu hebben wij als bestuur ook nog een ‘normale’ baan, we hebben een gezin, we hebben het best druk. Dus we missen nog wel eens een dergelijk mailtje. Wat moeten we dan doen? Het is een beetje flauw om de mensen die zich willen aanmelden heel lang te laten wachten. Dus we hebben afgesproken dat als het bestuur niet binnen 24 uur reageert, het proces er van uitgaat dat we deze persoon wel goed keuren. En dan gaat het proces weer verder.

Dat houdt dus in dat we in de gaten moeten houden wanneer een mail verstuurd wordt en dan moet er een timer gaan lopen. We moeten die 24 uur in de gaten houden.

We hebben nu een paar uitdagingen:

  1. Hoe pauzeren we een flow?
  2. Hoe triggeren we de flow zodat hij weer gaat lopen?
  3. Hoe bouwen we een time out in?
  4. Hoe zorgen we er voor dat het systeem veilig blijft en hack-vrij (we hebben te maken met professionele it’ers die langs willen komen, die vinden niets leuker dan het proces hacken).
  5. Hoe bouwen we dit zonder het overzicht kwijt te raken?

Als je mee bouwt, ga dan even naar de website van SendGrid. SendGrip is een API waarmee je emais kunt versturen via een eenvoudige REST API. Het account is gratis maar dan heb je een beperking van maximaal 100 berichten per dag. Voor ons is dat meer dan genoeg natuurlijk. Als je je aanmeldt, kun je een API key aanmaken. Ga daarvoor op de SendGrid site naar je Dashboard. In het menu links onderin staat “Settings”. Kies daar voor API Keys:

SendGrid Key management scherm

Klik dan op Create API Key. Hou de key geheim! Deel die niet! Je kunt ze altijd weer revoken maar met deze key kan iedereen op jouw account mails versturen. Er zijn heel veel spammers die dat best fijn zouden vinden.

Aanmaken nieuwe key voor SendGrid

De key moet een naam hebben, en je kunt bepalen wat je er mee kunt. In dit geval laat ik hem op Full Access staan.

De SendGrid key

Je krijgt nu een nieuwe key, Dit is de enige keer dat je hem te zien krijgt op de website van SendGrid dus maak er een kopie van (in jouw geval zal er geen zwarte balk over heen staan, maar ik wil mijn key toch geheim houden.) Ik sla de key op in mijn local.settings.json file zodat ik hem later kan uitlezen:

De SendGrid key in de settings file

Goed. Dat hebben we gedaan.

Wachten op events: twee stappen

Het proces is op te knippen in twee losse stappen:

  1. Het versturen van de email, waarna het proces wacht
  2. Het verder gaan van het proces nadat een link is geklikt of de time-out verstreken is.

Laten we eens beginnen met stap 1. Om precies te zijn: we gaan eerst de activity maken.

Aangezien we SendGrid gebruiken voor het versturen van de mail, moeten we de SendGrid Nuget package gebruiken. Laten we die eerst toevoegen. Er zijn er een aantal verschillende in de Nuget gallery, dus pak wel de goede. Namelijk: Microsoft.Azure.Webjobs.Extensions.SendGrid.

SendGrid Nuget package

Veilig omgaan met workflow instances

Het is tijd voor de activity. Zoals we al gezien hebben, krijgt een activity niet meer informatie dan dat hij nodig heeft om zijn werk te doen. Maar wat hebben we nu nodig in onze email-verzend activity? In ieder geval data over de bezoeker. Ik hou het minimaal en geef alleen het email adres van de bezoeker mee.

Maar: als het proces gepauzeerd wordt en daarna weer verder moet gaan, moet de workflow wel weten welke instance we verder willen laten gaan. Dat moet op de een of andere manier in de link gezet worden die we naar het bestuur versturen. Dus we moeten de instanceId van de workflow ook meegeven. We maken daar dus weer een DTO van:

namespace MeetingRegistration
{
    public class BoardEmailData
    {
        public string InstanceId { get; set; }
        public string Email { get; set; }
    }
}

Er is wel een puntje met betrekking tot de InstanceId. Dat is eigenlijk gevoelige informatie die je niet wilt delen met de buitenwereld. Ok, om iets zinnigs te doen met de workflow (weet je nog die statusQueryGetUri die we gebruiken?) moet je wel een secret key hebben maar als je die hebt EN je hebt de instance Id dan heb je ineens de mogelijkheid om het proces van buiten af te beinvloeden. Dus hoe minder info we delen met de buitenwereld hoe beter. Daarnaast: de InstanceId is een implementatie nummer; die hoor je sowieso niet te delen. We moeten dus wat anders verzinnen.

De oplossing is als volgt:

  1. We maken een storage ergens en daarin slaan we de InstanceId op.
  2. We gebruiken als key voor die entry in te storage een random, uniek nummer (een GUID zou mooi zijn).
  3. Als iemand de link klikt, dan geven we in die link de key mee (die GUID dus). De link verwijst naar een nieuwe Azure Function (niet durable, gewoon een GET based, HTTP Triggered Function zoals onze starter function).
  4. Als parameter hebben we in die function nu de GUID. Daarmee halen we de InstanceId van de workflow op
  5. We zeggen tegen de Durable Functions Runtime dat de workflow met dit unieke ID verder mag gaan, en we geven de approved of denied status mee.

Klinkt ingewikkeld? Valt heel erg mee!

We gaan met TableStorage werken, dus we moeten ook daarvoor de Nuget packages installeren: we gebruiken Microsoft.Azure..Webjobs.Extensions.Storage.

Storage Nuget extensions

Goed. Tijd voor wat code. Ik geef de lege activity alvast, dan doen we de implementatie later als ik uitgelegd heb hoe het een en ander werkt.

We hebben een class nodig die de data voorstelt die we gaan opslaan in de TableStorage.

using System;
using System.Collections.Generic;
using System.Text;

namespace MeetingRegistration
{
    public class StoredOrchestration
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public string OrchestrationId { get; set; }
    }
}

Deze class bevat de data die we op gaan slaan in de database. Als je nog niet eerder met TableStorage gewerkt heb: het is redelijk simpel.

De TableStorage heeft een Table (duh) en die tabel bevat records. Ieder record of entry heeft een composite key: een key die bestaat uit twee delen. Als eerste is er de Partitionkey en als tweede de Rowkey. We gebruiken een vaste PartitionKey om aan te geven dat deze entry gaat over een goed te keuren attendee. De Rowkey wordt die random GUID die we gebruiken. Als laatste heb je de mogelijkheid om zelf kolommen toe te voegen. Wij gebruiken er maar 1: de OrchestrationId die aangeeft welke instance van de workflow we gebruiken.

Waarom slaan we de rest van de data niet op, kun je je afvragen? Het ligt voor de hand om ook het email adres en/of de naam van de bezoeker op te slaan. Het antwoord is natuurlijk: we hebben het niet nodig. Als we de orchestrationId hebben, kan de orchestrator daarmee alle data ophalen die nodig is. Weet je nog? Als de activity klaar is, gaat de orchestrator opnieuw beginnen en haalt alle data op uit zijn eigen TableStorage. Daarin hebben we de naam en het email adres.

Laten we een begin maken met de activity zelf:

[FunctionName("A_SendBoardEmail")]
public static Task SendBoardEmail(
    [ActivityTrigger] BoardEmailData boardEmailData,
    [Table("Attendees", Connection = "AzureWebJobsStorage")] 
    out StoredOrchestration storedOrchestration,
    ILogger log)
{
    var uniqueKey = Guid.NewGuid().ToString("N");

    storedOrchestration = new StoredOrchestration()
    {
        PartitionKey = "AttendeeApprovals",
        RowKey = uniqueKey,
        OrchestrationId = boardEmailData.InstanceId
    };

    return Task.CompletedTask;
}

Er gebeurt nogal wat hier. We gaan hem even uit elkaar trekken.

Als eerste de FunctionName. Die kennen we nu wel. De activity zelf is een public static async method die een Task teruggeeft. De eerste parameter is de BoardEmailData die we net gemaakt hebben en die markeren we als ActivityTrigger. Tot zover geen rocket science. Dan de tweede parameter op regel 4 en 5: we maken gebruik van TableStorage. Hoe dit werkt is als volgt:

Door het gebruik van het attribuut [Table] geven we aan dat de data in storedOrchestration in de database moet komen of daar van uit moet komen. We geven de tabel naam mee (“Attendees”) en we geven de Connection mee. Dit is de naam van de setting in je local.settings.json file en bij ons staat dat op UseDevelopmentStorage=true. Dat is de default waarde, in productie verander je dat naar het echte storage account die je wilt gaan gebruiken.

Doordat we de StoredOrchestration als ‘out’ parameter aangeven, zal de runtime een entry in de tabel plaatsen als je een nieuwe instance van die class aanmaakt en de functie verlaten wordt. We hoeven dus niets te doen met connections, insert-statements, database code en de rest. De code die je nu ziet is genoeg om een entry in de TableStorage te plaatsen.

We maken een unieke sleutel aan. Dit is dus iets wat wel in de activity mag maar niet in een orchestration functie. We zitten in de activity dus dat werkt.

We maken een instance van de StoredOrchestration aan. De PartitionKey zet ik op AttendeeApprovals en de Rowkey op de nieuwe key die we net gemaakt hebben. We geven ook de orchestrationId mee die we mee gaan krijgen vanuit de orchestration.

Laten we dat gelijk maar even regelen: in de hoofd orchestration, na het aanroepen van de sub orchestration, plaatsen we de volgende regels:

if(!ctx.IsReplaying)
    log.LogWarning("Sending email to the dotNed board members.");

await ctx.CallActivityAsync("A_SendBoardEmail",
    new BoardEmailData {Email = attendee.Email, InstanceId = ctx.InstanceId});

Niets nieuws hier. We loggen (eenmalig!) en we roepen de activity aan met de data die we nodig hebben.

We moeten nu alleen de email nog versturen. Dat doen we in de activity en dat gaan we nu doen.

Om dat voor elkaar te krijgen moeten we de signature van de method aanpassen. Na de [Table] parameter maken we nu ook een [SendGrid] parameter:

[FunctionName("A_SendBoardEmail")]
public static Task SendBoardEmail(
    [ActivityTrigger] BoardEmailData boardEmailData,
    [Table("Attendees", Connection = "AzureWebJobsStorage")]
    out StoredOrchestration storedOrchestration,
    [SendGrid(ApiKey = "SendGridAPIKey")] 
    out SendGridMessage sendGridMessage,
    ILogger log)
{

We hebben net gezien dat als we een out parameter hebben en die markeren met [Table] dat die dan automatisch een entry aanmaakt in de TableStorage. Voor SendGrid hebben we een zelfde techniek: we definieren een out parameter van het type SendGridMessage (dit is de email) en we markeren deze met [SendGrid]. We geven de API key mee, dat is die in de local.settings.json staat (zie boven in deze post). De runtime zal deze er in zetten.

Dus: als we nu een instance aanmaken van deze SendGridMessage zal SendGrid deze automatisch versturen. Geen gedoe met het aanmaken van REST calls, aanroepen van de API enzovoorts. Komt ie!

[FunctionName("A_SendBoardEmail")]
public static Task SendBoardEmail(
    [ActivityTrigger] BoardEmailData boardEmailData,
    [Table("Attendees", Connection = "AzureWebJobsStorage")]
    out StoredOrchestration storedOrchestration,
    [SendGrid(ApiKey = "SendGridAPIKey")] 
    out SendGridMessage sendGridMessage,
    ILogger log)
{
    var uniqueKey = Guid.NewGuid().ToString("N");

    storedOrchestration = new StoredOrchestration
    {
        PartitionKey = "AttendeeApprovals",
        RowKey = uniqueKey,
        OrchestrationId = boardEmailData.InstanceId
    };

    var myHostName = System.Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME");

    var linkBase = @"http://{0}/api/processApproval/{1}/{2}";
    var approval = string.Format(linkBase, myHostName, uniqueKey, "true");
    var deny = string.Format(linkBase, myHostName, uniqueKey, "false");

    var body =
        "<html>" +
        "<h1>" +
        "Approval needed" +
        "</h1>" +
        "<p>" +
        $"Please approve at<a href= \"{approval}\" > this link</a>." +
        "<br />" +
        $"Or deny at<a href= \"{deny}\" > this link</a>" +
        "<br/>" +
        $"for user with email {boardEmailData.Email}." +
        "</p>" +
        "<p>" +
        "<h3>" +
        "Thanks!" +
        "</h3>" +
        "</p> " +
        "</html> ";

    var from = new EmailAddress("dennis@vroegop.org");
    var to = new EmailAddress("dotneddemo@outlook.com");

    sendGridMessage = new SendGridMessage()
    {
        From = from,
        Subject = "Please approve!",
        HtmlContent = body
    };
    sendGridMessage.AddTo(to);

    return Task.CompletedTask;
}

Ik geef je even de hele activity. Ik maak een paar strings aan voor de links. De host name vraag ik op via System.Environment.GetEnvironmentVariable(“WEBSITE_HOSTNAME”). In ons geval geeft dat de string “localhost:7071” terug. Die plakken we in de base string. Die base string zal er dus als volgt uit zien: http://localhost:7071/api/processApproval/%5BmyGuid%5D/true (of false bij afwijzen. Deze URL gaat verwijzen naar een nieuw te bouwen HTTP triggered Azure Function. Die maken we de volgende post wel.

Daarna maken we de HTML body voor de mail, zetten de adressen goed en maken een instance van de SendGridMessage aan. Dat zorgt ervoor dat de mail verzonden wordt als we de activity verlaten.

Als we dit nu runnen, zal het systeem een entry aanmaken in de tabel Attendees (als die nog niet bestaat, wordt die gelijk maar even aangemaakt) en daarna wordt er een email verstuurd. Ik verstuur hem even vanaf mijn eigen mail adres naar een dummy bestuurs-mailbox, dus pas dat aan.

In de Storage Explorer kun je het resultaat zien:

Storage Explorer met de nieuwe tabel en data

Je ziet: er is een nieuwe tabel aangemaakt en deze heeft nu de info over de orchestrationId die we moeten gaan goed- of afkeuren.

In mijn mailbox zie ik het volgende:

De link doet het niet natuurlijk, maar zou je er op klikken dan zie je in de URL dat de querystring parameter voor de unique key hetzelfde is als de Rowkey in de tabel. Dus in de nog te schrijven Azure Function kunnen we die data uit de table storage halen, de OrchestrationId ophalen en verder gaan.

Wachten…

Prima. Maar we wachten nog steeds niet en dat was het onderwerp van deze post. Hoe doen we dat? We moeten wachten tot iemand op een van die twee links klikt.

We gaan terug naar onze orchestrator. Na de call naar A_SendBoardEmail schrijven we de volgende code:

var boardHasApproved = await ctx.WaitForExternalEvent<bool>(
    "E_BoardHasApproved", 
    TimeSpan.FromSeconds(30), 
    true);

if (!boardHasApproved)
{
    return new RegistrationResult()
    {
        IsSucces = false,
        Reason = "Board did not approve. Sorry!"
    };
}

We roepen in de context de method WaitForExternalEvent<bool>(“E_BoardHasApproved”) aan. Er moet dus een externe event gebeuren willen we verder gaan. Tot die tijd is de orchestration niet meer levend, net als met activities. We geven een time-out mee van 30 seconden. Dat is wat kort om de board te laten reageren op de email, maar ik heb geen zin om voor iedere test 24 uur te moeten wachten. Idealiter sla je dit op in de Config (en lees die uit in een aparte activity!) maar nu doen we het even zo.

Als die timeout plaatsvind voor het event er is geweest, krijgen we een TimeOutException. Die kan je afvangen en melden dat er geen reactie is geweest op tijd. Maar wij hadden in ons geval al afgesproken dat als het bestuur niet binnen 24 uur (of 30 seconden zoals we hier doen) reageert dat we dan die aanvraag goedkeuren. Dus we geven een default return value van True mee aan onze WaitForExternalEvent. Na de timeout krijgen we dus automatisch true terug.

Laten we dit eens gaan testen.

We starten de flow, openen een browser met de juiste URL en laten hem gaan. Zo gauw de status pagina verschijnt, klikken we op de url bij statusQueryGetUri. Ik krijg dan het volgende resultaat.

// 20191104101214
// http://localhost:7071/runtime/webhooks/durabletask/instances/a05de0c734e64db181028163991c21f9?taskHub=DurableFunctionsHub&connection=Storage&code=rCJjUkbfRFrmoxIzf6CU8mxBnxUeOnLY5qr5DJ04C80FG87HgsDm6w==

{
  "name": "O_PerformRegistration",
  "instanceId": "a05de0c734e64db181028163991c21f9",
  "runtimeStatus": "Running",
  "input": {
    "$type": "MeetingRegistration.Attendee, MeetingRegistration",
    "Name": "Dennis",
    "Email": "dennis@vroegop.org",
    "WantsTweet": true
  },
  "customStatus": null,
  "output": null,
  "createdTime": "2019-11-04T09:12:08Z",
  "lastUpdatedTime": "2019-11-04T09:12:12Z"
}

Je ziet dat de status nu running is. En dat blijft hij ook tot we het proces verder laten gaan (nogmaals: next post!) of tot de timeout plaats vindt. Wacht even, doe een paar keer refresh van deze pagina en je ziet hem op een gegeven moment veranderen van Running naar Completed, met de bijbehorende data.

Je ziet hoe eenvoudig het is om te wachten op externe events en er een time out bij te bouwen. Je hoeft daar zelf eigenlijk niets voor te doen.

Onder water gebeurt er wel heel veel: er wordt een nieuw activity aangemaakt met een timer. Die timer gaat af als de tijd verlopen is. Als dat zo is, dan wordt er een message in de queue gezet, die de orchestrator krijgt. Dan wordt de activity die wacht op de message met approval (of denial) gestopt en het resultaat wordt genegeerd. Mocht er nu wel op tijd gereageerd zijn, dan zal de runtime de timer stoppen en disposen.

Dit is een hoop plumbing. Gelukkig hoeven we dat niet zelf te schrijven. Iemand die de code van de orchestrator ziet en een beetje kan programmeren, ziet meteen wat de flow is en hoe dat allemaal samenhangt.

Er is nog een dingetje: deel 2 van dit verhaal. Hoe gaan we nu zorgen dat dat event verstuurd wordt waar de activity zo met smart op zit te wachten?

Dat vind je in het volgende deel!