Van magazinethema naar projectpraktijk
Relevante dienst- en technische pagina's bij het artikel
Een Windows Service in Delphi lijkt in de dagelijkse praktijk vaak onspectaculair: draait op de achtergrond, verwerkt jobs, schrijft logs, spreekt databases of REST-API’s aan. Tot iemand „Dienst beëindigen“ klikt – of een patch-reboot nodig is – en de service niet netjes stopt. Dan toont de Diensten-console minutenlang „Wordt beëindigd…“, blijft de service hangen in de Stop Pending-status, en in het ergste geval wordt het proces hard beëindigd. Juist hier loont het om Windows Service in Delphi Graceful Shutdown als bewust architectuuronderwerp te behandelen: met een eenduidig shutdown-signaal, gedefinieerde timeouts en threads die daadwerkelijk reageren.
In dit artikel gaat het niet om framework-interna, maar om een praktijkgericht patroon: TEvent als stop-signaal (een kernel-nabij synchronisatieobject uit System.SyncObjs), gecombineerd met een Stop-Timeout-strategie die zowel de Windows Service Control Manager (SCM, dus de Windows-component die services start/stop) als je eigen worker-threads in acht neemt. Daarnaast typische randgevallen, debugging-aanpakken en de vraag wanneer de extra logica echt de moeite waard is.
Windows Service in Delphi Graceful Shutdown in de praktijk
De meest voorkomende oorzaak is eenvoudig: de service heeft minstens één thread die in een blokkerende operatie vastzit en geen afbreekpad kent. Klassiekers:
- Polling-lussen met Sleep: „while not Terminated do Sleep(1000)“. Bij stop komt het signaal binnen, maar de thread reageert pas na maximaal 1 seconde (of 30 seconden…).
- Blokkerende I/O: database-oproepen, HTTP-requests, Named Pipes, filesystem-waits – alles wat „gewoon wacht“ zonder op een stop-signaal te letten.
- Queue-consumers zonder wakeup: een worker wacht op een queue, maar bij stop wordt hij niet gewekt om uit te stappen.
- Lock-volgorde/deadlocks: bij stop wordt er „cleanup“ uitgevoerd terwijl andere threads nog locks vasthouden. Dit treedt graag alleen op in het stop-pad, omdat de volgorde daar anders is dan in de normale werking.
De Windows SCM verwacht dat een service snel reageert op een stop-commando en zijn status continu rapporteert (via SetServiceStatus; Delphi kapselt dat in de service-component). Als je wel een stop-event accepteert, maar je threads niet netjes afsluit, blijft het proces levend — en Windows bepaalt op een gegeven moment dat het „te lang duurt“. Het resultaat is dan óf een harde beëindiging, óf een service die in een onduidelijke tussentoestand blijft hangen.
Basisprincipe: een stop-signaal dat elke worker begrijpt
Een Graceful Shutdown werkt alleen als je een signaal hebt, dat:
- door alle relevante threads kan worden waargenomen,
In Delphi is TEvent daarvoor een zeer bruikbaar gereedschap: een event-object dat intern via Windows-handles uitgewerkt is (vergelijkbaar met CreateEvent/SetEvent). Je kunt het gebruiken als een „Stop requested“-signaal. Elke worker wacht dan niet blind, maar wacht „op werk of op Stop“.
TEvent juist kiezen: ManualReset vs. AutoReset
Bij stop-signalen wil je doorgaans Manual Reset (handmatig resetbaar): eenmaal gezet blijft het event „signaled“ totdat je het reset. Dat zorgt ervoor dat elke thread die later in een wachtfase komt het stop-signaal nog steeds herkent. Auto Reset zou hier riskant zijn, omdat het het signaal na één wachtende thread automatisch reset en andere threads het stop-signaal kunnen missen.
Delphi-Service-Lebenscyclus: Waar Stop echt aankomt
Een Delphi-Windows- en Linux-Services is typisch gebaseerd op TService (VCL/RTL). De SCM stuurt commando’s (Start, Stop, Pause, Continue). Delphi roept dan de overeenkomstige events/methoden aan (afhankelijk van het template bijv. OnStart, OnStop, OnExecute).
Belangrijk voor de architectuur:
- OnStop is geen plek voor lang wachten zonder statusupdates. Het is de plaats waar je de shutdown in gang zet en daarna gecontroleerd wacht – met timeout.
- OnExecute is vaak een lus. Als je daar „oneindig“ werkt, moet de lus op een stop-signaal reageren.
- Worker-Threads (TThread of thread-pools) moeten op hetzelfde stop-signaal reageren, anders is de service logisch gestopt maar fysiek nog niet klaar.
Net patroon: Stop-Event + Join van Worker + harde fallback
Het praktijkgerichte patroon bestaat uit vier stappen:
- Stop aanvragen: stop-event zetten, geen nieuwe jobs meer aannemen.
- Wakeups triggeren: als workers op queues of sleeps wachten, moeten ze „wakker kunnen worden“ (bijv. via event/queue-signaal).
- Geordend beëindigen: workers beëindigen hun lussen, sluiten resources (DB-verbindingen, bestanden, handles) en melden „klaar“.
- Timeout en fallback: als niet alles op tijd eindigt, moet je een beslissing nemen: verder wachten (met statusupdate) of gecontroleerd afbreken/hard beëindigen (afhankelijk van risico).
De kern is: geen thread mag uitsluitend op tijd wachten (Sleep) of uitsluitend op I/O blokkeren, zonder parallel rekening te houden met een stop-signaal. In plaats daarvan werk je met wachtfuncties die meerdere signalen verwerken (bijv. „Stop-Event of Work-Event“), of je kapselt I/O in timeouts plus stop-checks.
Stop-Timeout goed denken: SCM-Timeout vs. eigen Shutdown-Timeout
Hier ontstaan in projecten de meeste misverstanden. Er zijn twee verschillende timeout-niveaus:
- SCM-verwachting: Windows verwacht dat je in de status SERVICE_STOP_PENDING regelmatig voortgang meldt. Anders lijkt het alsof je vastzit. Delphi zorgt daar deels voor, maar zodra jij zelf langer blokkeert, heb je een strategie nodig om verdere status-updates mogelijk te maken (of om je stopfase kort te houden).
- Je eigen shutdown-timeout: Je definieert bijvoorbeeld “We geven ons 20 seconden om lopende jobs netjes af te ronden, daarna breken we af.” Dat is een architectuurbeslissing: gegevensconsistentie versus geforceerde reboot versus operationele voorschriften.
Praktisch betekent dit: je service moet snel in een toestand komen waarin hij geen nieuwe eenheden werk meer start, en vervolgens alleen nog wachten dat lopend werk eindigt – maar niet eindeloos. En deze wachtfase moet in kleine intervallen uitgevoerd worden, zodat je kunt reageren en eventueel loggen.
Hoe lang mag de stop duren?
Er bestaat geen magisch getal dat altijd past. Voor veel business-services is een doelbereik van 5–30 seconden realistisch: genoeg tijd voor in-flight data, maar kort genoeg voor patchvensters. Als je regelmatig langer nodig hebt, wijst dat vaak op te grote verwerkeenheden of op externe afhankelijkheden (DB/HTTP) zonder timeout.
Implementatie met TEvent: opzet die in productie stabiel blijft
Een beproefde opzet in de Delphi-service ziet er zo uit (zonder framework-details uit te melken):
- Een Stop-Event (TEvent, Manual Reset), dat bij stop wordt gezet.
- Een of meer worker-threads, die in hun hoofdloop regelmatig op Stop controleren.
- Optioneel een Work-Event of een queue die werk signaleert. Workers wachten dan op “Work of Stop”.
- Een shutdown-fase die workers ‘joined’ (dus wacht tot ze beëindigd zijn), maar met timeout.
Het cruciale is niet of je TThread, omnithreadlibrary of een eigen pool gebruikt, maar dat je workers niet “blind” blijven draaien. Een worker-loop zou structureel zo moeten zijn: wachten op gebeurtenis(sen) → werk in kleine chunks → tussen chunks Stop controleren → resources netjes vrijgeven.
Valkuil: Terminate alleen is niet genoeg
Veel Delphi-threads worden met Terminate “afgebroken”. Dat is echter slechts een flag. Als de thread op dat moment in een blokkerende API zit, gebeurt er in eerste instantie niets. Daarom is een eigen Stop-Event zo nuttig: je kunt het in wachtroutines integreren en gerichte wakeups veroorzaken.
Valkuil: FreeOnTerminate in service-context
In services zie je vaak FreeOnTerminate := True. Dat kan werken, maar maakt shutdown moeilijker controleerbaar, omdat je vaak geen nette referentie meer hebt om op het thread-einde te wachten en fouttoestanden te loggen. Voor gecontroleerde stoplogica is het doorgaans stabieler om threads expliciet te bezitten en tijdens shutdown deterministisch te wachten en vrij te geven.
Blokkerende operaties: zo maak je ze stopbaar
Het lastige deel is niet het Event zelf, maar de plekken waar je service blokkeert. Drie typische categorieën:
1) Sleep/Polling vervangen: wachten met Stop-Event
Als je periodiek werkt („elke 10 seconden controleren“), gebruik dan niet Sleep(10000), maar wacht op een event met timeout. Dan kan je Stop-Event het wachten onmiddellijk beëindigen. Dat verkort de stop-latentie en voorkomt het gevoel „service reageert niet“.
2) Queue-consumer: Work-Event + Stop-Event combineren
Als je een producer/consumer-architectuur hebt (bijv. jobs worden in een queue geplaatst), heb je een signaal nodig dat consumers wakker maakt. Vaak is dat een extra TEvent („Work available“). De consumer wacht dan op twee handles: „Work“ of „Stop“. Bij Stop zet je het Stop-Event en, indien nodig, ook het Work-Event, zodat alle consumers gegarandeerd uit de wait komen.
3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade
Bij databasetoegangen of HTTP-calls bepaalt dit of je service netjes stopt. Voor de operatie geldt: Geen call zonder timeout. Een timeout is geen luxe, maar voorwaarde voor bestuurbaarheid. Daarnaast moet je tussen retries/backoff-fasen altijd op Stop controleren. Anders krijg je het klassieke: „service stopt niet, omdat hij net 10 retries met Sleep doet“.
Bij sommige bibliotheken kun je annuleringen expliciet triggeren (bijv. query-annulering). Als dat niet mogelijk is, moet je ten minste zo configureren dat timeouts kort genoeg zijn om de shutdown-timeout niet te overschrijden.
Stop Pending korrekt: Status, Logging und Erwartungsmanagement
Als een service stopt, is het operationeel belangrijk te begrijpen, waar hij vastzit. Daarvoor heb je twee dingen nodig:
- Log-markers in het stoppad: „Stop aangevraagd“, „geen nieuwe jobs“, „wacht op worker“, „Worker X beëindigd“, „shutdown voltooid“.
- Meetbare tijden: Hoe lang duurt het stoppen? Welke fase kost tijd? Vaak volstaat al een monotone tijdmeting zoals GetTickCount64 of TStopwatch (monotoon = niet vertekend door systeemtijdwijzigingen).
Als je in het Stop-Pfad slechts één enkele logregel „Stopping…“ wegschrijft, blijft het debuggen in het veld een raadspel. In de service-operatie zijn logs vaak het enige dat je zonder interactie krijgt.
Welke logs zijn in services echt nuttig?
- Service-PID, starttijd, versie/build (zonder overdreven overhead).
- Aantal actieve workers, aantal in-flight jobs.
- Actieve externe afhankelijkheden: „DB-call loopt“, „HTTP-request loopt“, „bestand-flush loopt“ (alleen geaggregeerd, niet elk detail).
- Stop-Timeout bereikt: welke workers zijn nog open?
Debugging in het veld: reproduceerbaar maken in plaats van gokken
Stop-problemen doen zich vaak alleen in productie voor: andere load, andere latenties, andere rechten, andere patch-vensters. Enkele in de praktijk geteste hefbomen:
Service gecontroleerd testen
- Stop tijdens actieve verwerking (niet in idle).
- Stop tijdens externe storing: DB kort niet bereikbaar, trage HTTP-endpoint, fileshare weg.
- Stop direct na start (race-conditions: workers nog in opbouw).
Event Viewer en Service Control Manager Signale
Windows schrijft dienstgebeurtenissen, maar die zijn vaak grof. Beter is dat je service zelf naar een logbestand of het Windows Event Log schrijft. Belangrijk daarbij: logging moet in het Stop-Pfad nog werken. Als je logger tijdens shutdown te vroeg vrijgeeft of flush blokkeert, verlies je precies die beslissende sporen.
Hangende threads zichtbaar maken
Als je herhaaldelijk „Stop Timeout“ ziet, loont een blik op thread-statussen (bijv. met een debugger/Procdump in een testomgeving). Vaak vind je dan een thread in een wait-toestand op een handle dat nooit gesignaleerd wordt, of in een netwerk-call zonder timeout. De fix is dan zelden „nog meer sleep“, maar een nette afbreekroute.
Wanneer is de inspanning echt de moeite waard?
Een minimalistische dienst met alleen een timer en zonder externe afhankelijkheden kan soms „gewoon stoppen“. Zodra echter één van de volgende punten van toepassing is, is een nette Graceful Shutdown vrijwel altijd de moeite waard:
- De service verwerkt jobs met bijwerkingen (bestanden schrijven, DB-transacties, API-calls).
- Er zijn meerdere threads of een pool.
- De service hangt aan netwerkbronnen (DB, REST, message broker, fileshares).
- De operatie vereist planbare onderhoudsvensters (reboots, updates, failover).
De meerwaarde is niet „elegantie“, maar operationele betrouwbaarheid: minder harde procesafbrekingen, minder inconsistente tussenstanden, minder handmatige ingrepen.
Praktijk-valkuilen: wat bij shutdown vaak misgaat
1) Stop wordt gezet, maar nieuwe jobs blijven binnenkomen
Als je binnenkomend werk accepteert (bijv. via socket, bestand-trigger, timer), moet je in het Stop-Pfad eerst het accepteren van nieuw werk stoppen: listeners sluiten, timers uitzetten, scheduler pauzeren. Anders loop je het einde achterna omdat er nog steeds nieuwe jobs starten.
2) Cleanup blokkeert (Flush, Close, Finalize)
„Snel nog alles flushen“ kan in de service-context gevaarlijk zijn als het doel (netwerkschijf, remote log, DB) op dat moment hangt. Daarom: cleanup ja, maar met een begrensde tijd. Desnoods moet je beslissen welke data je in-memory verliest, in plaats van de volledige stop te blokkeren.
3) Locks en volgorde
Bij Stop raak je vaak dezelfde datastructuren aan als de workers (Queues, Caches, States). Als de Stop-thread locks vasthoudt en vervolgens op het einde van workers wacht, terwijl workers dezelfde lock nodig hebben, ontstaat een Stop-Deadlock. Tegenmaatregelen: houd lock-hold-tijden kort, wacht in het Stop-pad niet „onder een lock“, definieer een duidelijke volgorde.
4) Concurrentie bij dubbele Stop
In de praktijk kan Stop meerdere keren getriggerd worden (bijv. Stop + Shutdown, of Stop komt opnieuw). Je Stop-pad moet idempotent zijn: het zetten van het Stop-Event is oké, maar dubbele Join/Free-logica moet goed beschermd zijn (bijv. via een atomische vlag).
Operationeel perspectief: was beheerders en IT-leads van de service verwachten
Voor operatie en administratie gaat het uiteindelijk niet om hoe „mooi“ de code is, maar of de dienst:
- bij Stop betrouwbaar eindigt (planbaar, zonder vastlopers),
- bij Stop geen inconsistente data produceert (bijv. halve bestanden, open transacties),
- in geval van fouten bruikbare logs levert,
- bij onderhoudsvensters en deployments voorspelbaar is.
Dat is ook de reden waarom het onderwerp Stop-Timeout niet slechts „ontwikkelaarswerk“ is: het beïnvloedt patchcycli, recovery-tijden en de vraag of geautomatiseerde deployments überhaupt mogelijk zijn.
Concreet kader voor een robuust Shutdown-Design
Als je het onderwerp pragmatisch wilt standaardiseren, hebben deze richtlijnen zich bewezen:
- Een globaal Stop-Event, Manual Reset, vroeg in de levenscyclus van de service aangemaakt, laat vrijgegeven.
- Geen Sleep in worker-loops zonder een stop-ondersteunend alternatief (Wait met timeout).
- Alle externe calls met timeouts (DB, HTTP, Fileshares). Kies timeouts zo dat ze binnen je shutdown-timeout passen.
- Stop-timeout als configuratie (bijv. in INI/Registry), zodat operatie kan reageren zonder opnieuw te compileren.
- Fasesmodel: eerst graceful (lopende jobs afmaken), dan optioneel „soft abort“ (geen nieuwe stappen), daarna harde exit als laatste redmiddel.
- Goede Stop-Logs met fasen en tijdmetingen.
Conclusie: TEvent + Stop-Timeout is geen luxe, maar bestuurbaarheid
Een vastlopend Stop is zelden een eenmalige fout – meestal is het een architectuurgat: werk draait in threads of blokkerende calls die geen gemeenschappelijk Stop-signaal kennen. Met een duidelijk Stop-Event (TEvent, Manual Reset), stop-ondersteunende waits in plaats van Sleep, consequente timeouts voor externe afhankelijkheden en een gedefinieerd shutdown-timeout krijg je een service die in de praktijk voorspelbaar is.
De inzet van deze code betaalt zich vooral uit als je service in productieomgevingen met onderhoudsvensters, geautomatiseerde deployments of kritische neveneffecten draait. Dan is „Graceful Shutdown“ geen cosmetica, maar een bouwsteen voor stabiele operatie en minder escalaties bij de volgende reboot.
Als je jullie Stop-pad eens goed wilt opzetten of een bestaande Delphi-service wilt controleren op robuuste Shutdown-logica en operationele veiligheid, is een technische sparrings-call vaak de snelste weg naar concrete maatregelen: neem contact op.
Voor dit onderwerp zijn ook Delphi Windows Service en Tevent Delphi belangrijk. Het artikel plaatst deze aspecten begrijpelijk in context en laat zien waar het in de dagelijkse praktijk om gaat.
volgende stap
Wanneer het onderwerp een concreet project wordt, moeten architectuur, bestaande omgeving en exploitatie vroegtijdig samen worden bekeken.
We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.
- Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
- REST, toegang tot gegevens, portalen en rollout worden niet naar latere fasen verschoven.
- U ziet vroeg welke weg economisch en operationeel levensvatbaar is.