Vorige week heb ik een artikel gedeeld over Hangfire en dat ik mijn eigen uitbreiding wilde bouwen. Dit omdat ik geen SQL Server database beschikbaar had, ik met de "in memory versie" geen historie meer had, dus ik wilde kijken of dit in eerste instantie via tekstbestanden opgeslagen kon worden en later na een herstart weer in kon laden.
Dat ging op zich best wel goed, maar ik sloot het artikel af met "FetchJob wordt intern door Hangfire regelmatig aangeroepen, 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!".
Later... dat is nu, een week later. Want ik wil mijn eigen website hiermee laten draaien, maar elke minuut een fout in het foutenlog, dat wil ik eigenlijk niet.
Wat heeft dat te maken met "developers kunnen geen uren schatten"? Nou, het was eigenlijk de bedoeling dat die site op 6 januari weer zou draaien. Even "Hangfire fixen" en dan door met de andere punten die ook nog gedaan moeten worden. Mijn inschatting was dat het wel zou lukken, want hoe moeilijk kan het zijn? Nou, zoals je in het andere blog en in het onderstaande kunt lezen, dan kom je zaken tegen die "nieuw" zijn en niet even snel te fixen zijn. En dan kies je soms voor een korte route, even binnendoor. Maar dat geeft dus de fouten in het log: je hebt dus weer een nieuw probleem om op te lossen. Aargh! Scott Hanselman heeft daarom zijn podcast-serie ook de "Hanselminutes" genoemd. Als hij zegt dat iets een half uur (30 minuten) gaat duren, reken dan maar een factor 6 of iets dergelijks (exacte getal weet ik niet meer). Als je developer bent, ben je waarschijnlijk ook (vaak) geconfronteerd met een projectmanager of iemand anders binnen de organisatie die zegt "kun je dit en dit even maken, hoeveel tijd heb je nodig?". Naast het feit dat we vaak alleen de "happy flow" zien en op basis daarvan de uren geven, vaak als je alles mee telt en de schatting geeft die wel aardig in de buurt van het echte aantal uren komt, krijg je als antwoord "maar zoveel tijd heb je daar toch niet voor nodig?". In ieder geval, ik ga door met deze case en het tot een succesvol einde brengen. Alleen nog niet in deze post ;)
Als ik in de code van Hangfire duik, dus in de code voor SQL Server, dan zie ik ook dat het niet alleen het doorlopen van de queues is en een taak die staat te wachten terug te geven is. En als die er niet is, om dan maar NULL terug te geven.
Nee, er wordt gewerkt met een SQLServerJobQueue class die een implementatie is van interface IPersistentJobQueue. Binnen die class heb je een private ConcurrentDictionary die een Tuple van SqlServerStorage, de naam van de queue en een AutoResetEvent heeft. Bij een aanroep op FetchJob wordt hier een item aan toegevoegd (of opgevraagd als die al bestaat op basis van de naam van de queue) en dat resultaat wordt dan weer toegevoegd aan een array van type WaitHandle.
Dat komt binnen een while-loop die net zolang wacht tot er een cancel-request binnen komt. Of er moet binnen die while een taak beschikbaar komen, dan wordt die terug gegeven. Binnen die while-loop wordt een WaitHandle.WaitAny(array van WaitHandle items, 200 milliseconden vertraging) aangeroepen wat er ook weer voor zorgt dat er gewacht wordt tot er iets met dat item gebeurt.
Hier zit je op redelijk "diep" niveau met threads / processen te werken. Ontwikkelaars die dit vaker gedaan hebben zullen hier vast hun hand niet voor omdraaien, maar dit is iets wat ik zelf nog niet eerder aangepakt heb. Ik heb ooit een Windows-service met Threads gemaakt, maar dat zijn redelijk basic zaken. Hier werk je met uitgewerkte classes, WaitHandles. Het is goed dat ik in deze code duik (ik moet hier later zeker wat mee gaan doen, ook zelf wat ompielen en in andere posts mijn voorbeelden en uitleg geven), maar het is niet iets wat ik nu werkend ga maken/krijgen (omdat ik de site waar ik dit voor gebruik redelijk snel online wil krijgen en dit te lang gaat duren).
Ik heb in mijn eigen code ook even snel een "simpele implementatie" gemaakt die dit principe volgt. Ik had al mijn twijfels, daarom heb ik dit eerst lokaal een tijdje laten draaien. Maar goed ook, want ik zie het geheugengebruik wat normaal rond de 450 / 500 MB ligt langzaam maar zeker oplopen naar 950 MB (en als je maar lang genoeg wacht naar meer).
Dus wat ga ik nu doen? Nou, ik ga eerst mijn "oude implementatie" volgen, als er geen taak is die uitgevoerd moet worden, geef ik een NULL terug. Dat zorgt (natuurlijk) dat de fouten weer in het Umbraco-log worden weggeschreven.
Tenmiste, dat zou gebeuren bij de standaard implementatie. Ik wil de fouten afvangen, en als dat betrekking heeft op deze NULL waarde de fout "onderdrukken".
De fout die gegenereerd wordt, dat wordt in het achtergrondproces gedaan, dat is allemaal Hangfire code. Dus ik ben gaan kijken of Hangfire daar iets voor biedt. In de online documentatie staat hoe je zelf een log-class kunt maken en die koppelt in je Program.cs. Ik heb het voorbeeld gevolgd en kon zo de fouten succesvol "onderdrukken".
Met mijn eigen implementatie onderdruk ik nu die fout. Alleen, als er een andere fout optreedt, zou ik die eigenlijk wél weer willen zien in het Serilog van Umbraco. Op deze pagina kun je lezen hoe je naar het log kun "schrijven". Ik zie alleen niet hoe ik in mijn code bij dat log kan komen. Ik zou natuurlijk een call naar een eigen API endpoint kunnen uitvoeren die daar zorgt voor het loggen van de fout, maar dat is voor mij nu even teveel werk. Dan maar even "alle fouten onderdrukken", via het /hangfire-endpoint kan ik altijd zelf controleren of herhalende taken nog uitgevoerd worden en of ze succesvol zijn of mislukken.
Dat heb ik gedaan en vervolgens heb ik op mijn laptop de site laten draaien. De onderhoudspagina komt ervoor, dus dat gaat goed. Vervolgens ben ik nog even wat zaken gaan inrichten in de Umbraco-omgeving en kijk nog eens in Visual Studio. Ik zie hier dat het geheugengebruik inmiddels weer richting de 800 MB opgelopen is. Na een sanitaire stop en het ophalen van een nieuw biertje (het is zaterdagavond als ik hiermee bezig ben), zie ik dat we inmiddels boven de 900 MB zitten. Het is duidelijk, in mijn code zit ergens een "memory-leak" en ook daar ga ik geen extra tijd aan besteden.
In mijn Github-repo had ik al de waarschuwing toegevoegd dat je het niet in productie moet gebruiken. En dat ga ik nu dus ook niet doen. Ik heb mijn code uitgeschakeld en de Hangfire.InMemory actief gemaakt. Dan maar mijn historische data "kwijt" na een recycle, dat is altijd beter dan het vollopen van je geheugen en daardoor het volledig onbereikbaar worden van mijn website. Mijn code in Program.cs is nu dit geworden:
//var customStorage = new CustomMemoryAndFileStorage(System.IO.Path.Combine(Environment.CurrentDirectory, "App_Data"));
//builder.Services.AddHangfire(configuration => configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180).UseSimpleAssemblyNameTypeSerializer().UseRecommendedSerializerSettings().UseStorage<CustomMemoryAndFileStorage>(customStorage).UseLogProvider(new HangFireUmbracoLogProvider()));
//builder.Services.AddHangfireServer(customStorage);builder.Services.AddHangfire(configuration => configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_180).UseSimpleAssemblyNameTypeSerializer().UseRecommendedSerializerSettings().UseInMemoryStorage());
builder.Services.AddHangfireServer();
Vervolgens ben ik nog even bezig geweest in Umbraco en heb ik gekeken wat het geheugengebruik doet. Ook nu lijkt het wat op te lopen, maar het blijf (nu) rond de 576 MB hangen. Ik ga er vanuit dat dit stabiel blijft.
Waarschijnlijk zouden er developers geweest zijn die even wat zaken snel getest hadden, gezien hadden dat de taken goed uitgevoerd werden en vervolgens de boel "live" gezet hadden. Waarbij er na een tijdje dus geheugenproblemen zouden ontstaan. En dat zijn toch wel zo'n beetje de moeilijkste problemen om op te sporen (naast de deadlock problemen op tabellen).
Dus als de vraag gesteld wordt: "hoe lang duurt het om dit te bouwen", zorg dus ook altijd dat je de tijd neemt om te testen wat jouw wijziging qua impact heeft op de performance / het geheugengebruik van de applicatie!
Ik heb hiermee laten zien dat ik met een foutieve implementatie en met wat "shortcuts" dacht een werkend product te bouwen, maar waarbij ik eigenlijk "alleen maar" geheugenproblemen geïntroduceerd heb in een systeem wat daarvoor wél goed werkte.
Nog even een toevoeging: in .NET kun je gebruik maken van nugets: codebibliotheken die andere gebruikers ter beschikking stellen, zodat ook "de rest van de wereld" daarvan gebruik kan maken. Geweldig, want het scheelt een hoop werk, je hoeft niet zelf alles te bouwen, je hebt een kant-en-klare product wat je kunt gebruiken. Maar zoals ik deze fouten geïntroduceerd heb (niet opzettelijk), zo kunnen er ook "bugs" in die code zitten. Dus nog even de aanvullende waarschuwing, prima dat je code "van anderen" gebruikt om je project werkend te krijgen (en als het een partij als Microsoft is die de nuget aanbiedt, dan zal het wel goed zijn), maar zorg wel dat het tot het minimum beperkt wordt.