Hangfire is een codebibliotheek waarmee je taken uit kunt voeren. En dat is fijn. Want daarmee kunt je zorgen dat bepaalde controles en acties via een "achtergrond-proces" uitgevoerd worden. Daardoor hebben bezoekers "geen last" van acties die uitgevoerd worden. Want je had soms wel eens implementaties waarbij zodra een bezoeker op de site kwam er taken werden uitgevoerd. En dat zorgde ervoor dat die bezoeker langer op de pagina moest wachten welke hij/zij aan het openen was. Of denk bijvoorbeeld aan het orderproces, je bestelt wat via een website en als resultaat van je aankoop moet er een mailtje naar jou en de leverancier gestuurd worden en er moet ook wat in een XML bestand opgeslagen worden. Vaak werden die acties allemaal in "de flow van de bezoeker" uitgevoerd. Dus het duurt langer voor hij/zij de bedankpagina ziet. Of, als er wat fout gaat, krijgt de persoon die de aankoop uitgevoerd heeft een foutmelding te zien en vraagt zich af of de betaling/levering nu wel goed uitgevoerd wordt.
Daarbij is de website / de "web-applicatie" vaak een monolytisch systeem: alle functies zitten in één project. Handig om te testen, maar het maakt de boel wat minder "wendbaar". Door zulke (moeilijk te onderhouden) systemen ontstonden de microservices. Het afboeken van de voorraad, het versturen van de mailtjes, dat waren functies die "ergens anders draaiden", dus je dropte een bericht in een queue en je kon door met je eigen code. Hangfire zit in ons geval in hetzelfde project, maar het is wel een achtergrondproces, dus ook hier kun je opdrachten in de queue droppen en het wordt uitgevoerd. Of het mislukt en dan kunnen er automatisch nog x pogingen ondernomen worden om de taak succesvol uit te voeren. En als het uiteindelijk toch niet lukt, dan kun je via het dashboard van Hangfire (en mogelijk zelf nog eigen acties/koppelingen) zien dat een taak mislukt is.
Informatie en documentatie over Hangfire kun je op de site vinden: hangfire.io. Het is een .NET / .NET Core code-bibliotheek, dus als je naar iets op zoekt bent wat niet betrekking op deze frameworks heeft (ruby, PHP, python), dan zul je op zoek moeten naar wat anders.
Als je het standaard Hangfire-package installeert (in Visual Studio in je project via een rechtermuisknop op je project, Manage NuGet Packages... en dan via Browse zoeken op Hangfire) dan kom je uit op het pakket van odinserj. Standaard worden je taken en hun resultaten opgeslagen in een SQL-Server database (die je koppelt in de opstartcode van je applicatie).
Voor mijn persoonlijke project waarvoor ik Hangfire wil gebruiken loop ik tegen de beperkingen aan. Ik heb namelijk geen SQL Server draaien. Met een dikke 200 euro per maand voor mijn hosting wil ik eigenlijk niet ook nog eens geld betalen voor een SQL -Server instantie. Ik heb een mySQL-server, dus daar kan ik databases op aanmaken, Umbraco draait op noSQL, kan ik daar niet de Hangfire taken in opslaan?
Bij TRES hadden we ook een project wat mijn collega Erwin uitgewerkt heeft voor een Gallery in Visual Studio. Dat moet een soort feed genereren en om daar nu een complete database aan te koppelen, dat vonden we een beetje zonde. We hebben toen een ander package toegevoegd waarmee je "in memory" je taken kon laten draaien. Dat is ook een officieel package van odinserj trouwens, zoek op "Hangfire.InMemory".
Op zich zou dat wel een oplossing zijn die ik zou kunnen gebruiken. Alleen, als er een herstart van je applicatie is ben je jouw historie kwijt. En dat wil ik niet, je hebt een mooi dashboard bij Hangfire en daar kun je de historie in terugzien. Want ik wil nog kunnen controleren of een taak de afgelopen 24 uur wel elke minuut uitgevoerd is.
Toevallig liep ik in de opstartcode aan tegen de methode .UseStorage<TStorage>(...). Daarmee kun je jouw eigen storage koppelen! Weet je wat, ik ga mijn taken in een tekstbestand opslaan! Tenminste, ik begin met het opslaan van data in tekstbestanden en ga daarna kijken of ik zelf zaken in geheugen op kan slaan. Acties op het filesystem zijn altijd trager dan iets uit geheugen lezen, dus als zaken werken kan ik de code alsnog aanpassen van tekstbestand naar geheugen.
Als ik een "jongere developer" was geweest had ik misschien deze uitleg in ChatGPT gegooid en had ik kant-en-klare code gekregen om dit werkend te krijgen. Moet ik misschien later nog maar eens doen om te kijken of dat gewerkt zou hebben.
Maar... ik ga het zelf bouwen. Het is namelijk net zoiets als hoofdrekenen. Je kunt een lijst met getallen overtypen op je zakrekenmachine en dan het antwoord noteren. Maar het zelf doen, uit je hoofd optellen van de scores die je bij een potje Scrabble met je moeder hebt behaald, zorgt ervoor dat je brein actief blijft. Als ik betaal bij de kassa en het niet gepast betaal, dan zit binnen 5 tellen in mijn hoofd hoeveel wisselgeld ik terug moet krijgen.
Zo is het ook met programmeren. In het .NET Framework zitten zoveel functies en mogelijkheden en meestal gebruik je dezelfde functies. Een aantal jaren geleden kwam LINQ als onderdeel in .NET en inmiddels werkt iedereen met collecties en de standaard functies die je daarbij kunt gebruiken. Zelf "iets nieuws maken" zorgt dat je tegen zaken aanloopt die je nog niet kende of misschien wel kende en waarvan je denkt "oh, kan het ook zo?".
Ik ga een eigen code-bibliotheek voor deze uitbreiding maken. En ik heb nu ook meteen een test-project toegevoegd. Dat vond ik vroeger "moeilijk" en "teveel werk", maar als je weet hoe het moet, is het een fluitje van een cent. En het heeft me flink wat ellende bespaard. Bij 1 van de functies in de Hangfire-code was namelijk het resultaat van een functie een string (tekstwaarde). Als ik niets kon vinden, gaf ik een lege tekstwaarde terug. Maar daardoor werd die lege waarde als een geldige taak beschouwd, wat (natuurlijk) mislukte, 10 keer opnieuw geprobeerd om uit te voeren en inmiddels was er een minuut voorbij en kwam de volgende taak erbij: het geheugen loopt zo naar de 2 GB! In dit geval moet je een NULL waarde terug geven.
Dat was de korte samenvatting, hierbij de volledige beschrijving van hoe ik het aangepakt heb en onderaan de link naar mijn Github-repo voor dit project.
Het begin was makkelijk. Die TStorage, dat is een class die je zelf aanmaakt en laat erven van Hangfire.JobStorage. Zodra je dat gedaan hebt krijg je de melding dat je abstracte methode GetConnection en GetMonitoringApi niet geïmplementeerd hebt. Deze voeg je toe. En als je dat doet moet je nog 2 classes maken, 1 die de implementatie is van interface IStorageConnection en de andere is de implementatie van interface IMonitoringApi. Zodra je die aanmaakt krijg je de melding dat niet alle functies van de interfaces geïmplementeerd worden, die laat je automatisch toevoegen. Dan geeft ie bij elke methode een NotImplementedException. Als je die weghaalt, krijg je al beeld op /hangfire. Bepaalde functies werken dan niet, het aantal servers staat ook op 0 (ook als ik de functie AnnounceServer implementeer).
Omdat het de bedoeling was om dit "even snel te doen" en het nu toch wel wat extra tijd kost, ga ik even voor de shortcut. En dat is de code van Hangfire bekijken. Het project staat namelijk op Github, net als de InMemory-versie. Ik zoek daarin op de functies, dan kan ik "spieken" bij de implementaties van de functies voor SQL Server. Dat gaat voorspoedig.
Het wegschrijven naar een tekstbestand van bepaalde zaken (herhalende-taak.txt, hash_herhalende-taak.txt, job-bestand.txt, job-parameters-bestand.txt) laat je zien wanneer acties uitgevoerd worden en welke data het oplevert. De taak die ik elke minuut uitvoer voegt een regel met een timestamp toe in een tekstbestand. Dus ik kan ook makkelijk controleren of de taak uitgevoerd wordt.
Een aantal acties schrijf ik meteen al weg naar het geheugen en niet naar een tekstbestand. Er worden namelijk records in de Counter-tabel toegevoegd. Dat schreef ik weg naar counter.txt. Maar zoals te verwachten was, zijn er een aantal workers/threads die dit gelijktijdig proberen te doen, je zit dan met een gelockt bestand e.d., dus dat moet je dan ook niet doen.
Mijn "herhalende taak" wordt netjes 1x uitgevoerd. Alleen... hij wordt niet herhaald. In hash_herhalende-taak.txt zie ik ook de reden:
System.ArgumentNullException: Value cannot be null. (Parameter 'cronExpression')
En er onder het aantal retry-pogingen: RetryAttempt 5
Na wat zoeken de oorzaak gevonden. Ik overschreef elke keer het bestand hash_herhalende-taak.txt, maar je moet alle regels doorlopen. Als er geen nieuwe waarde meegegeven wordt, moet die bestaande waarde weer weggeschreven worden in het bestand! Daardoor verdween eerder de regel Cron * * * * * uit het bestand. Na deze aanpassing wordt de taak wél voor een tweede keer uitgevoerd. Trouwens, pas nadat ik ook de functie GetNextJobFromQueues aangepast had, ik gaf hier het eerste item in de Queue terug, dat moet de laatste worden. Tenminste, voor nu, want ik moet zaken uit de Queue opruimen. En dat heb ik vervolgens ook gedaan, het object in code netjes naar een Queue-class omgezet, zodat ik met .Enqueue(..) een item kan toevoegen en met .Dequeue(..) zorg dat het element weer uit de lijst verwijderd wordt.
Hierna mijn site even laten draaien, als de taak 5x succesvol na elkaar uitgevoerd is, vind ik het wel prima. Ik ga me nu richten op de Monitoring. Ik had in public StatisticsDto GetStatistics() al gezorgd dat de aantallen getoond worden. Ik kon in het tekstbestand zien dat er 5 regels in waren weggeschreven, maar dus ook in het overzicht stond bij Processed het getal 5. Klik je er dan op, dan krijg je echter een lege lijst. Om dit werkend te krijgen moet ik public JobList<SucceededJobDto> SucceededJobs(int from, int count) implementeren.
Op dit punt "leer ik ook wat nieuws". Ik moet namelijk in die functie een Hangfire.Common.Job- en een InvocationData-object aanmaken. De constructor verwacht een aantal parameters die ik zo niet weet te bepalen, dus ik vraag me af hoe ik die objecten dan aan kan maken. Maar in de code van Hangfire zie ik dat dit simpel kan met var j = default(Hangfire.Common.Job) en var i = default(InvocationData). Die default(..)-functie kende ik nog niet. Natuurlijk ken ik wel vanuit LINQ de FirstOrDefault() en daar heeft dit ook betrekking op. Daarom is het goed dat je eens niet een standaard component gebruikt, maar zelf aan de slag gaat en in code duikt. En ook kijkt wat er in de code van het Hangfire-project zelf staat. Want zodra je bekend bent geworden met een bepaalde functie komt het vaak voor dat je deze in de toekomst steeds meer kunt/gaat gebruiken.
Inmiddels heb ik code die niet volledig is, maar wel "werkt". Dit was een POC, Proof of Concept, dus "kan ik het werkend krijgen"? Het antwoord is "ja". Er zijn nogal wat functies die niet ondersteund worden in mijn code (maar schijnbaar niet geraakt worden/gebruikt worden) en ook het laden van de historie heb ik nog niet ingebouwd. Deze POC-code zat in een nieuwe Umbraco-omgeving die ik aan het opzetten ben, dus dat ga ik straks verwijderen. Eerst ga ik met de kennis die ik nu opgedaan heb een project opzetten en dit via Github delen.
De code heb ik uitgewerkt, daarbij zet ik meer data in het geheugen. Omdat ik wil testen/controleren of alles werkt heb ik ook een testproject toegevoegd. Met onderstaande code start je een application op die blijft draaien, dus perfect om je Hangfire-server te testen!
[Test]
public void TestRecurringJob()
{
var builder = WebApplication.CreateBuilder(new string[] { });var myStorage = new CustomMemoryAndFileStorage(SaveLocation);
builder.Services.AddHangfire(configuration => configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180).UseSimpleAssemblyNameTypeSerializer().UseRecommendedSerializerSettings().UseStorage<CustomMemoryAndFileStorage>(myStorage));
builder.Services.AddHangfireServer(myStorage);var app = builder.Build();
app.UseHangfireDashboard();RecurringJob.AddOrUpdate("check-for-maintenance", () => TestWriteToFileTask.WriteToOutput(), Hangfire.Cron.Minutely());
app.Run();
}
Hoewel de data die ik weg schrijf tussen de 2kB en 20kB is zie ik in de test echter het geheugengebruik oplopen naar 2 GB! Daar zit dus ergens een memory-leak in mijn code. Je kunt via het tabblad Diagnostic Tools in Visual Studio een dump maken en vervolgens die bekijken. Als dat opgelopen is tot 2 GB, dan ben je al te laat, de boel blijft maar laden, laden, laden.... Normaal bleef het verbruik rond de 35 MB hangen. Dus als je rond de 70 MB zit, dan kun je ook een snapshot maken. Als ik dan naar de objecten op de heap kijk staat bovenaan StringBuilder met rond de 50.000 objecten. Ik gebruik in mijn code geen StringBuilder, dus ik vermoed dat dit uit de core-code van Hangfire komt. Daar wordt wel een aantal maal een StringBuilder gebruikt. Ik vermoed dat er in mijn code iets niet goed gaat, dat er Excepties in de Hangfire-code optreden en daardoor er (veel) StringBuilder-objecten aangemaakt worden.
Het was even zoeken, maar uiteindelijk is de oorzaak gevonden. De functie GetFirstByLowestScoreFromSet heeft als resultaat een variabele van type string. Dus als ik niets kon vinden, dan gaf ik een string.Empty terug. Dat werd in de aanroepende code als een geldig ID van een job beschouwd. Dus er werden acties uitgevoerd, maar daar kon niets mee gedaan worden. In de debug-info zag je ook dat taak met id '' verwijderd werd. Door null terug te geven als er geen taak was, daarmee loste ik dit probleem op.
Ook zie ik in mijn testproject nog een fout. Maar het lijkt erop dat dit "by design" is. Het gaat namelijk om FetchJob. Als er geen taak terug te geven is, dan geef ik NULL terug. De code van Hangfire vindt dat niet lekker, omdat er in de code van Hangfire iets gebeurt met job.JobId. En als job gelijk is aan NULL, dan geeft .JobId een foutmelding. Nu ik de code in mijn Umbraco-omgeving heb gekoppeld, zie ik het elke minuut in het Umbraco foutenlog terugkomen. Dus ik zal hier iets anders voor moeten bedenken. Dat is voor later!
Dit is de repo: hangfire-memory-and-textfile-history. Als je er zelf mee aan de slag wilt gaan, een fork maken o.i.d., ga je gang!