Hangfire is een prima tool, op een actieve website.

Ingediend door Dirk Hornstra op 06-mar-2023 21:02

Zoals je waarschijnlijk vaker gehoord hebt, "monolitische applicaties", die zijn slecht, "micro-services" zijn de standaard. Maar goed, je hebt web-applicaties die in het verleden gebouwd zijn en vaak dit stramien volgen:

De klant heeft zijn/haar bestelling betaald via iDeal en keert terug naar de website. Hier vinden de volgende processen plaats:

  • er wordt geregistreerd dat de klant betaald heeft
  • de producten worden van de voorraad afgeboekt
  • er wordt een e-mail verzonden naar de klant "bedankt voor uw aankoop!"
  • de gegevens worden weggeschreven naar een XML-bestand, dit wordt later geïmporteerd door het ERP-systeem
  • de klant wordt doorgestuurd naar de pagina "bedankt voor uw aankoop"


Maar je zult het zien, er zijn zoveel bestellingen dat het afboeken niet lukt (deadlocks e.d.). De klant krijgt een foutmelding, de pagina "er is een fout opgetreden" of een wit scherm te zien. Hij/zij ontvangt niet de e-mail. Het XML-bestand wordt niet aangemaakt.

De klant belt met de leverancier ("ik heb betaald, maar heb nog geen bevestiging, krijg ik mijn bestelling wel?"). De leverancier kan de succesvolle betaling terugvinden, maar niet zien of de mail verzonden is, of het afboeken gelukt is en stelt deze vraag... bij de leverancier van het pakket/de site.
Omdat alles zo "verweven" zit kan de service-afdeling daar ook niet even de mail opnieuw versturen, het XML bestand opnieuw aanmaken, dus developer X moet hier een los stuk script voor uitvoeren om dit alsnog goed te krijgen. Vervelend als 1 zo'n aankoop niet goed gegaan is. Een groot probleem als het bij 2.580 aankopen fout gegaan is en developer X dit dus 2.580x mag gaan herhalen.

Daarom zijn micro-services zo fijn, je kunt de "losse onderdelen" onafhankelijk van elkaar uitvoeren. Nu is het natuurlijk wel wat werk om zo'n applicatie helemaal uit elkaar te trekken, dit in losse API's te zetten en aan te roepen. En dan heb je nog het mogelijke probleem dat er iets fout gaat en wil je dat loggen en/of opnieuw proberen.

Hangfire is daarvoor een mooie tool. In plaats van dat je die "services" ergens anders laat draaien, daar je code hebt, kun je dit binnen je eigen project houden, Hangfire zorgt dat de taken uitgevoerd worden, je hebt een dashboard en als taken mislukken, dan wordt er nog een keer geprobeerd om het proces uit te voeren. En nogmaals. Tot maximaal 10x. Ik heb met de standaard waardes gewerkt, mogelijk kun je dit ook nog aanpassen naar andere waardes.

Alleen, dit moet wel binnen een website draaien die "actief" is. Ik was hier zelf al eens tegen aan gelopen en dacht toen dat ik niet goed had gekeken of dat het aan onze aanpak lag. Namelijk voor het publiceren van een installatie-pakket voor plug-ins binnen Visual Studio. Dat zip-bestand werd niet aangemaakt, keek je binnen het Hangfire-dashboard, dan werd het wel aangemaakt. Nu hadden we in dat project geen database gebruikt, alleen een "in-memory-store", dus ik dacht dat het daar mogelijk aan lag. Maar toen was ik later met een ander project bezig, waarbij mails bij reserveringen en retouren werd gestuurd en ik ook geen mails ontving, pas als ik in het dashboard van Hangfire keek, ontving ik ze wel.

Mijn collega Robert Haitsma is met een nieuw project bezig en liep hier nu ook tegen aan (en was zo slim om de database te controleren en niet via de site naar hangfire te gaan) en constateerde dat de tabel Hangfire.Servers na een uur leeg was. We kunnen dit alleen maar koppelen aan het feit dat na een x periode in IIS application-pools in een soort "hybernate-state" komen als er geen verkeer is. Prima voor "gewone sites", maar hier ongewenst. Via Google al wat mogelijke oplossingen gevonden, maar die hebben niet onze voorkeur. Want dan maak je de site afhankelijk van andere sites die de website "aanroepen" of instellingen in IIS. En in dat laatste geval, na een aantal jaar worden de webservers vervangen, wijkt deze site af van de normale sites, wordt die instelling mogelijk niet meegenomen en loop je na migratie eerst weer tegen problemen aan.

Dus ... kijk dan eens hoe andere applicaties dit aanpakken. Toevallig was ik voor een probleem in het log van Umbraco aan het kijken en zag daar een "keep-alive taak", een interne call naar /umbraco/api/keepalive/ping. Kijk, als we zorgen dat bij opstarten er altijd een herhalende taak is, die elke 5 minuten een "ping" doet op een eigen URL, dan blijft de applicatiepool actief en worden ook de andere taken uitgevoerd op het moment dat dit zou moeten.

Dat hebben we nu toegevoegd.

In 2022 ben ik bij Techorama geweest in Utrecht en voor de presentatie van Sander Hoogendoorn kreeg ik nog net wat mee van de presentatie van Rob Richardson over "Minimal API's". Even gespiekt in zijn Github-repo voor zijn project/demo: link.

Dat gebruikt in onze code, we hadden nog een structuur van Startup.cs en Program.cs, maar omdat app dan een Interface was, had die geen geldige app.Map(...) functie, dus alles samengevoegd in Program.cs.

 

// dit in een eigen bestand, globale variabele voor de aan te roepen URL
 public class KeepAliveSettings
{
     public static string KeepAliveUrl => "/api/keepalive/ping";
}

// deze class in een eigen bestand
public class HangfireBuilderHelper
{
     /// <summary>
     /// Call our domain to keep the application pool "alive"
     /// </summary>
     /// <param name="baseUrl">URL of this Web Application</param>
     /// <returns></returns>
     public static async Task KeepAlive(string baseUrl)
     {
         using (var client = new HttpClient())
         {
             await client.GetStringAsync($"{baseUrl.TrimEnd(new char[] { '/' })}{KeepAliveSettings.KeepAliveUrl}");
         }
     }
}

// Program.cs
....
app.Map(KeepAliveSettings.KeepAliveUrl, () => "pong");
....
RecurringJob.AddOrUpdate(() => HangfireBuilderHelper.KeepAlive(builder.Configuration["ProjectUrl"]), "*/5 * * * *");
app.Run();
 

Ik heb nog even gekeken of ik die HttpClient-call ook "binnen" de lambda-expressie van de AddOrUpdate() kon krijgen, want dan had ik de code heel beknopt kunnen houden, maar daar kreeg ik foutmeldingen op.
Zo eerst maar gelaten, met deze aanpassingen hebben we de boel een weekend laten draaien en lijkt het erop dat de boel nu wel "automagisch" blijft draaien!

Ook als je wel voldoende bezoekers op je site hebt is het ook aan te raden om dit te implementeren, want hierdoor ben je niet afhankelijk van mogelijke bezoekers op je site en hoef je jezelf ook niet af te vragen waarom een taak die om 2.00 had moeten draaien pas om 8.30 uur werd gestart: toen had je pas de eerste bezoeker op je website!

Appendix 1: ... oh nee!

Op zondagmiddag nog even gecontroleerd en toen was de tabel Hangfire.Server weer leeg! In de Hangfire.Job-tabel kon ik gelukkig zien dat het een programmeerfoutje was, op een bepaald moment werd niet de eigen URL aangeroepen, maar een localhost... en dan wordt de web-applicatie niet meer geraakt. Dat aangepast en toen dacht ik dat het klaar was. Maar... vanmorgen kon Robert mij melden dat de boel weer leeg was. En dankzij de Job-tabel konden we zien rond welke tijd dit was. En ook toen was de oorzaak duidelijk: 's nachts worden onze applicationpools "gerecycled". En als er dan geen aanroep op de applicatie uitgevoerd wordt, dan wordt het proces dus niet opnieuw opgestart.

Dus zo komen we toch weer terug bij de titel van deze post, "je hebt een actieve website" nodig... om deze website te triggeren. Dus we doen dat nu via onze Zabbix-monitoring, want dat is PHP, draait in Apache en draait tot het einde der tijden. Let op, je hoeft hier dus niets voor open te zetten! Jouw applicatie mag gerust een 401 of 403 statuscode terug geven, om die status terug te kunnen geven moet je web-applicatie draaien. Dus zelfs een aanroep van iets "wat er niet bij mag komen" kan ervoor zorgen dat jouw applicatie blijft werken. Daar zal nu onze keuze op vallen omdat dit uit te voeren is zonder in IIS gekke dingen te doen, een eigen keep-alive-ping in het project en een regelmatige externe aanroep vanuit Zabbix.

Appendix 2: ... of gebruik de Hangfire manier.

Dit probleem is natuurlijk niet nieuw en Hangfire heeft er zelf ook een artikel aan gewijd: link. Het aanpassen van een omgeving in Azure is makkelijk, gewoon de slider op "always on" laten staan. Bij de andere oplossing, het aanpassen van de applicationHost.config, daar ben ik niet zo'n voorstander van. Wordt door alle sites gebruikt, bij fouten gaan waarschijnlijk al je sites onderuit. Volgens mij hebben we dit wel gebruikt om bepaalde applicationpools op 32 bit in te stellen. En het andere alternatief is dat je een module installeert en bij de applicatie-pools instelt dat ze altijd actief zijn.