Od témy magazínu k projektovej praxi
Súvisiace stránky služieb a technológií k príspevku
Služba Windows Service in Delphi pôsobí v každodennej prevádzke často nespektakulárne: beží na pozadí, spracováva úlohy, zapisuje logy, komunikuje s databázami alebo s REST-APIs. Až kým niekto neklikne na „Zastaviť službu“ – alebo nenastane reštart po patchi – a služba sa neukončí čisto. Potom konzola služieb niekoľko minút zobrazuje „Prebieha zastavenie…“, služba uviazne v Stop Pending-stave a v najhoršom prípade je proces tvrdo ukončený. Práve tu sa oplatí brať Windows Service in Delphi Graceful Shutdown ako vedome riešenú architektonickú tému: s jasným shutdown-signálom, definovanými timeoutmi a vláknami, ktoré skutočne reagujú.
V tomto príspevku nejde o interné detaily frameworku, ale o prakticky použiteľný vzor: TEvent ako Stop-Signal (synchronizačný objekt blízko jadra z System.SyncObjs), skombinovaný so Stop-Timeout-stratégiou, ktorá zohľadňuje nielen Windows Service Control Manager (SCM, teda Windows-komponentu, ktorá služby štartuje/zastavuje), ale aj tvoje vlastné worker-vlákna. Plus typické okrajové prípady, prístupy k debugovaniu a otázka, kedy sa dodatočná logika skutočne oplatí.
Windows Service in Delphi Graceful Shutdown v praxi
Najčastejšia príčina je jednoduchá: služba má aspoň jedno vlákno, ktoré je uviaznuté v blokujúcej operácii a nepozná cestu na prerušenie. Klasika:
- Polling-Schleifen mit Sleep: „while not Terminated do Sleep(1000)“. Pri zastavení príde signál, ale vlákno zareaguje až po až 1 sekunde (alebo 30 sekundách…).
- Blockierende I/O: Volania do databázy, HTTP-requests, Named Pipes, Filesystem-waits – všetko, čo „jednoducho čaká“, bez toho aby sledovalo Stop-Signal.
- Queue-Consumer ohne Wakeup: Worker čaká na frontu, ale pri zastavení nie je prebudený, aby mohol ukončiť činnosť.
- Lock-Reihenfolge/Deadlocks: Pri zastavení sa robí „Cleanup“, zatiaľ čo iné vlákna ešte držia zámky. To sa často prejaví len v ceste ukončenia, pretože poradie tam je iné než počas normálnej prevádzky.
Der Windows SCM očakáva, že služba na Stop-príkaz zareaguje rýchlo a priebežne nahlasuje svoj stav (cez SetServiceStatus; Delphi to zapuzdruje v service-komponente). Ak síce príjmeš Stop-udalosť, ale vlákna nesprávne vypneš, proces zostane nažive – a Windows nakoniec rozhodne, že to „trvá príliš dlho“. Výsledkom je potom buď tvrdé ukončenie, alebo služba, ktorá uviazne v nejasnom medzistave.
Grundprinzip: Ein Stop-Signal, das jeder Worker versteht
Ein Graceful Shutdown funktioniert nur, wenn du ein Signal hast, das:
- von allen relevanten Threads beobachtet werden kann,
- pôsobí aj pri blokujúcich čakacích stavoch,
- v stop-ceste deterministický je (žiadna „možno sa raz dostane von“-nádej),
- má jasnú stratégiu timeoutov.
In Delphi ist TEvent dafür ein sehr brauchbares Werkzeug: ein Event-Objekt, das intern über Windows-Handles umgesetzt ist (vergleichbar mit CreateEvent/SetEvent). Du kannst es als „Stop requested“-Signal verwenden. Jeder Worker wartet dann nicht einfach blind, sondern wartet „auf Arbeit oder auf Stop“.
TEvent richtig wählen: ManualReset vs. AutoReset
Bei Stop-Signalen willst du üblicherweise Manual Reset (manuell zurücksetzbar): Einmal gesetzt, bleibt das Event „signaled“, bis du es zurücksetzt. Damit ist sichergestellt, dass jeder Thread, der später in eine Wartephase kommt, das Stop-Signal trotzdem erkennt. Auto Reset wäre hier riskant, weil es das Signal nach einem wartenden Thread automatisch zurücksetzt und andere Threads das Stop-Signal verpassen könnten.
Delphi-Service-Lebenszyklus: Wo Stop wirklich ankommt
Ein Delphi-Windows- und Linux-Services basiert typischerweise auf TService (VCL/RTL). Der SCM schickt Kommandos (Start, Stop, Pause, Continue). Delphi ruft dann entsprechende Events/Methoden auf (je nach Template z. B. OnStart, OnStop, OnExecute).
Wichtig für die Architektur:
- OnStop ist kein Ort für langes Warten ohne Status-Updates. Es ist der Ort, an dem du den Shutdown anstößt und dann kontrolliert wartest – mit Timeout.
- OnExecute ist oft eine Schleife. Wenn du dort „endlos“ arbeitest, muss die Schleife auf ein Stop-Signal reagieren.
- Worker-Threads (TThread oder Thread-Pools) müssen auf dasselbe Stop-Signal reagieren, sonst ist der Service logisch gestoppt, aber physisch noch nicht fertig.
Sauberes Muster: Stop-Event + Join der Worker + harter Fallback
Das praxistaugliche Muster besteht aus vier Schritten:
- Stop anfordern: Stop-Event setzen, keine neuen Jobs mehr annehmen.
- Wakeups auslösen: Wenn Worker auf Queues oder Sleeps warten, müssen sie „aufwachen“ können (z. B. über Event/Queue-Signal).
- Geordnet beenden: Worker beenden ihre Loops, schließen Ressourcen (DB-Verbindungen, Dateien, Handles) und melden „fertig“.
- Timeout und Fallback: Wenn nicht alles rechtzeitig endet, musst du eine Entscheidung treffen: weiter warten (mit Status-Update) oder kontrolliert abbrechen/hart beenden (je nach Risiko).
Der Kern ist: kein Thread darf ausschließlich auf Zeit warten (Sleep) oder ausschließlich auf I/O blockieren, ohne parallel ein Stop-Signal zu berücksichtigen. Stattdessen arbeitest du mit Wartefunktionen, die mehrere Signale berücksichtigen (z. B. „Stop-Event oder Work-Event“), oder du kapselst I/O in Timeouts plus Stop-Checks.
Stop-Timeout richtig denken: SCM-Timeout vs. eigener Shutdown-Timeout
Hier passieren in Projekten die meisten Missverständnisse. Es gibt zwei verschiedene Timeout-Ebenen:
- Očakávanie SCM: Windows očakáva, že v stave SERVICE_STOP_PENDING budeš pravidelne hlásiť priebeh. Inak to bude vyzerať, akoby si sa zasekol. Delphi sa o to čiastočne stará, ale akonáhle sa sám dlhšie zablokuješ, potrebuješ stratégiu, ako naďalej umožňovať aktualizácie stavu (alebo ako svoju stop-fázu udržať krátku).
- Tvoj vlastný shutdown-timeout: Definuješ napr. „Dávame si 20 sekúnd na čisté dokončenie bežiacich úloh, potom ukončíme.“ Je to architektonické rozhodnutie: konzistencia dát vs. nútený reštart vs. prevádzkové požiadavky.
V praxi to znamená: tvoj servis by sa mal rýchlo dostať do stavu, v ktorom nezačína žiadne nové pracovné jednotky, a potom len čaká, kým bežiaca práca neskončí – ale nie večne. A táto čakacia fáza by mala prebiehať v malých intervaloch, aby si mohol reagovať a prípadne logovať.
Ako dlho môže trvať zastavenie?
Neexistuje žiadne magické číslo, ktoré by vždy platilo. Pre mnohé business-servisy je realistický cieľový rozsah 5–30 sekúnd: dosť času pre „in-flight“ dáta, ale dostatočne krátko pre patch-okná. Ak pravidelne potrebuješ viac, často to naznačuje, že spracúvaš príliš veľké jednotky naraz alebo že externé závislosti (DB/HTTP) bežia bez timeoutu.
Implementácia s TEvent: architektúra, ktorá v prevádzke zostáva stabilná
Overená štruktúra v Delphi-servise vyzerá takto (bez rozpitvávania detailov frameworku):
- Jedno Stop-Event (TEvent, Manual Reset), ktoré sa nastaví pri zastavení.
- Jeden alebo viac Worker-Threads, ktoré vo svojej hlavnej slučke pravidelne kontrolujú Stop.
- Voliteľne jedno Work-Event alebo fronta (Queue), ktorá signalizuje prácu. Workeri potom čakajú na „Work alebo Stop“.
- Jedna Shutdown-Phase, ktorá Workeri „joined“ (teda čaká, kým sa ukončia), ale s timeoutom.
Dôležité nie je, či používaš TThread, omnithreadlibrary alebo vlastný pool, ale že tvoje Workery nebývajú „slepé“. Worker-loop by mal štrukturálne vyzerať takto: Čakanie na udalosť(y) → práca v malých dávkach → medzi dávkami kontrola Stop → čisté uvoľnenie zdrojov.
Úskalie: Terminate samotné nestačí
Mnohé Delphi-vlákna sa prerušia pomocou Terminate. To je však len flag. Ak sa vlákno nachádza v blokujúcom API volaní, najprv sa nič nestane. Preto je vlastné Stop-Event také užitočné: môžeš ho integrovať do čakacích volaní a cielene vyvolať prebudenia.
Úskalie: FreeOnTerminate v kontexte služby
V službách sa často používa FreeOnTerminate := True. Môže to fungovať, ale robí shutdown ťažšie kontrolovateľným, pretože často už nemáš čistú referenciu na čakanie na ukončenie vlákna a na protokolovanie chybových stavov. Pre riadenú logiku zastavenia je zvyčajne stabilnejšie explicitne vlastniť vlákna a pri shutdown deterministicky čakať na ich ukončenie a uvoľniť ich.
Blokujúce operácie: ako ich spraviť prerušíteľnými
Zložitejšia časť nie je samotná udalosť, ale miesta, kde sa tvoja služba blokuje. Tri typické kategórie:
1) Sleep/Polling nahradiť: Wait s Stop-Event
Ak pracuješ periodicky („kontrola každých 10 Sekunden“), nepoužívaj Sleep(10000), ale čakaj na udalosť s timeoutom. Vtedy môže tvoje Stop-Event okamžite ukončiť čakanie. To zníži latenciu pri zastavení a zabráni dojmu „služba nereaguje“.
2) Queue-Consumer: kombinácia Work-Event + Stop-Event
Ak máš Producer/Consumer-architektúru (napr. úlohy sú vložené do fronty), potrebuješ signál, ktorý prebudí consumerov. Často je to ďalší TEvent (Work available). Consumer potom čaká na dva Handles: „Work“ alebo „Stop“. Pri Stop nastavíš Stop-Event a prípadne aj Work-Event, aby všetci consumeri určite vyšli z čakania.
3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade
Pri prístupe do databázy alebo HTTP-volaniach sa rozhoduje, či tvoja služba zastaví čisto. V prevádzke platí: Žiadne volanie bez timeoutu. Timeout nie je luxus, ale predpoklad ovládateľnosti. Okrem toho by si mal medzi Retries/Backoff- fázami vždy kontrolovať Stop. Inak dostaneš klasiku: „Služba nezastaví, pretože práve robí 10 Retries s Sleep“.
V niektorých knižniciach môžeš prerušenia explicitne vyvolať (napr. zrušenie dotazu). Ak to nejde, musíš aspoň nakonfigurovať timeouts tak, aby boli dostatočne krátke a neprekročili denoted Shutdown-Timeout.
Stop Pending správne: Stav, logovanie a riadenie očakávaní
Keď sa služba zastavuje, je z prevádzkového hľadiska dôležité vedieť, kde sa zasekáva. Na to potrebuješ dve veci:
- Log značky v stop-ceste: „Stop angefordert“, „keine neuen Jobs“, „warte auf Worker“, „Worker X beendet“, „Shutdown fertig“.
- Merateľné časy: Ako dlho trvá zastavenie? Ktorá fáza zhltá čas? Tu často stačí monotónne časové meradlo ako GetTickCount64 alebo TStopwatch (monotonické = nedeformované zmenami systémového času).
Ak v stop-pathed zapíšeš len jediný log záznam „Stopping…“, zostáva ladenie v teréne hrou na hádanie. V prevádzke služieb sú logy často jediné, čo dostaneš bez interakcie.
Ktoré logy sú v službách naozaj užitočné?
- PID služby, čas spustenia, verzia/build (bez nadmerného overheadu).
- Počet aktívnych workerov, počet in-flight jobov.
- Aktívne externé závislosti: „DB-Call prebieha“, „HTTP-Request prebieha“, „zápis súboru (Flush) prebieha“ (len agregovane, nie každý detail).
- Dosiahnutý stop-timeout: ktoré workery sú ešte otvorené?
Ladenie v teréne: reprodukovateľné namiesto hádania
Problémy pri ukončení sa často prejavia len v produkcii: iná záťaž, iné latencie, iné práva, iné časové okná patchovania. Niekoľko praxou overených páčok:
Testovať službu pod kontrolou
- Stop počas aktívneho spracovania (nie v idle).
- Stop počas externej poruchy: DB krátko nedostupná, HTTP-endpoint pomalý, súborové zdieľanie nedostupné.
- Stop bezprostredne po štarte (race-conditions: workery sú ešte v inicializácii).
Protokoly udalostí a signály Service Control Managera
Windows zapisuje udalosti služby, ale tie sú často hrubé. Lepšie je, keď tvoja služba sama zapisuje do log súboru alebo do Windows Event Logu. Dôležité je pri tom: logging by mal v stop-pathed ešte fungovať. Ak uvoľníš logger počas shutdownu príliš skoro alebo ak flush blokuje, stratíš práve tie rozhodujúce stopy.
Zobraziť zablokované vlákna
Ak opakovane vidíš „Stop Timeout“, oplatí sa pozrieť na stavy vlákien (napr. pomocou debuggeru / Procdump v testovacom prostredí). Často nájdeš vlákno v stave čakania na handle, ktorý nikdy nie je signálovaný, alebo v sieťovom volaní bez timeoutu. Riešenie zriedka spočíva v „ďalšom sleep-e“, skôr v čistom ukončovacom mechanizme.
Kedy sa ten náklad skutočne oplatí?
Minimálna služba, ktorá má len timer a žiadne externé závislosti, sa môže niekedy „jednoducho zastaviť“. Akonáhle však platí niektoré z nasledujúcich, oplatí sa väčšinou implementovať čistý Graceful Shutdown:
- Služba spracováva joby so vedľajšími účinkami (zapisovanie súborov, DB transakcie, API volania).
- Sú tu viaceré vlákna alebo pool.
- Služba závisí na sietových zdrojoch (DB, REST, Message Broker, súborové zdieľania).
- Prevádzka vyžaduje plánovateľné okná údržby (rebooty, aktualizácie, failover).
Pridaná hodnota nie je „elegancia“, ale prevádzková istota: menej tvrdých prerušení procesov, menej nekonzistentných medzistavov, menej manuálnych zásahov.
Praktické úskalia: Čo sa pri shutdown-e často pokazí
1) Stop sa nastaví, ale nové joby sa aj tak prihadzujú
Ak prijímaš prichádzajúcu prácu (napr. cez socket, súborový trigger, timer), musíš v stop-pathed najprv zastaviť prijímanie novej práce: zavrieť listener, deaktivovať timery, pozastaviť scheduler. Inak budeš doháňať koniec, pretože stále štartujú nové joby.
2) Cleanup blokuje (Flush, Close, Finalize)
„Len rýchlo všetko flushnúť“ môže v kontexte služby byť nebezpečné, ak cieľ (sieťový disk, remote-log, DB) práve visí. Preto: cleanup áno, ale s limitovaným časom. V núdzi sa musíš rozhodnúť, ktoré dáta stratíš v pamäti, namiesto toho, aby si blokoval celý stop.
3) Zámky a poradie
Beim Stop greifst du häufig auf dieselben Datenstrukturen zu wie die Worker (Queues, Caches, States). Wenn der Stop-Thread Locks hält und dann auf Worker-Ende wartet, während Worker denselben Lock brauchen, hast du einen Stop-Deadlock. Gegenmittel: Lock-Hold-Zeiten klein halten, im Stop-Pfad nicht „unter Lock warten“, klare Reihenfolge definieren.
4) Nebenläufigkeit beim doppelten Stop
In der Praxis kann Stop mehrfach getriggert werden (z. B. Stop + Shutdown, oder Stop kommt erneut). Dein Stop-Pfad sollte idempotent sein: Stop-Event setzen ist okay, aber doppelte Join/Free-Logik muss sauber geschützt werden (z. B. über ein Atomik-Flag).
Operativer Blick: Was Admins und IT-Leads vom Service erwarten
Für Betrieb und Administration zählt am Ende nicht, wie „schön“ der Code ist, sondern ob der Dienst:
- bei Stop verlässlich endet (planbar, ohne Hänger),
- bei Stop keine inkonsistenten Daten produziert (z. B. halbe Dateien, offene Transaktionen),
- im Fehlerfall brauchbare Logs liefert,
- bei Wartungsfenstern und Deployments berechenbar ist.
Das ist auch der Grund, warum das Thema Stop-Timeout nicht nur „Entwicklerkram“ ist: Es beeinflusst Patchzyklen, Recovery-Zeiten und die Frage, ob automatisierte Deployments überhaupt möglich sind.
Konkrete Leitplanken für ein robustes Shutdown-Design
Wenn du das Thema pragmatisch standardisieren willst, haben sich diese Leitplanken bewährt:
- Ein globales Stop-Event, Manual Reset, früh im Service-Lebenszyklus erstellt, spät freigegeben.
- Kein Sleep in Worker-Loops ohne Stop-fähige Alternative (Wait mit Timeout).
- Alle externen Calls mit Timeouts (DB, HTTP, Fileshares). Timeouts so wählen, dass sie in deinen Shutdown-Timeout passen.
- Stop-Timeout als Konfiguration (z. B. in INI/Registry), damit Betrieb reagieren kann, ohne neu zu kompilieren.
- Stufenmodell: Erst graceful (laufende Jobs zu Ende), dann optional „soft abort“ (keine neuen Schritte), dann harter Exit als letzter Ausweg.
- Gute Stop-Logs mit Phasen und Zeitmessung.
Fazit: TEvent + Stop-Timeout ist kein Luxus, sondern Steuerbarkeit
Ein hängender Stop ist selten ein Einzelfehler – meist ist es ein Architekturloch: Arbeit läuft in Threads oder blockierenden Calls, die kein gemeinsames Stop-Signal kennen. Mit einem klaren Stop-Event (TEvent, Manual Reset), stop-fähigen Waits statt Sleep, konsequenten Timeouts für externe Abhängigkeiten und einem definierten Shutdown-Timeout bekommst du einen Service, der im Alltag berechenbar ist.
Der Code lohnt sich besonders, wenn dein Service in Produktionsumgebungen mit Wartungsfenstern, automatisierten Deployments oder kritischen Seiteneffekten arbeitet. Dann ist „Graceful Shutdown“ nicht Kosmetik, sondern ein Baustein für stabilen Betrieb und weniger Eskalationen beim nächsten Reboot.
Wenn du euren Stop-Pfad einmal sauber aufsetzen oder einen bestehenden Delphi-Service auf robuste Shutdown-Logik und Betriebssicherheit überprüfen willst, ist ein technischer Sparrings-Call oft der schnellste Weg zu klaren Maßnahmen: Kontakt aufnehmen.
Für dieses Thema sind auch Delphi Windows Service und Tevent Delphi wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
ďalší krok
Keď sa z témy stane reálny projekt, architektúru, existujúci stav a prevádzku treba včas posudzovať spoločne.
Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.
- Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
- REST, prístup k údajom, portály a nasadenie nebudú odložené na neskôr ako následné úlohy.
- Včas identifikujete, ktorá cesta je ekonomicky a prevádzkovo životaschopná.