Від теми журналу до практики проєкту
Відповідні сторінки послуг і технічні сторінки до публікації
Ein Windows Service in Delphi wirkt im Alltag oft unspektakulär: läuft im Hintergrund, verarbeitet Jobs, schreibt Logs, spricht Datenbanken oder REST-APIs an. Bis jemand „Dienst beenden“ klickt – oder ein Patch-Reboot ansteht – und der Service nicht sauber stoppt. Dann zeigt die Dienste-Konsole minutenlang „Wird beendet…“, der Service hängt im Stop Pending-Status, und im schlimmsten Fall wird der Prozess hart beendet. Genau hier lohnt es sich, Windows Service in Delphi Graceful Shutdown als bewusstes Architekturthema zu behandeln: mit einem klaren Shutdown-Signal, definierten Timeouts und Threads, die wirklich reagieren.
In diesem Beitrag geht es nicht um Framework-Interna, sondern um ein praxistaugliches Muster: TEvent als Stop-Signal (ein Kernel-nahes Synchronisationsobjekt aus System.SyncObjs), kombiniert mit einer Stop-Timeout-Strategie, die sowohl den Windows Service Control Manager (SCM, also die Windows-Komponente, die Dienste startet/stoppt) als auch deine eigenen Worker-Threads berücksichtigt. Dazu typische Randfälle, Debugging-Ansätze und die Frage, wann sich die zusätzliche Logik wirklich lohnt.
Windows Service in Delphi Graceful Shutdown in der Praxis
Die häufigste Ursache ist simpel: Der Service hat mindestens einen Thread, der in einer blockierenden Operation steckt und keinen Abbruchpfad kennt. Klassiker:
- Polling-Schleifen mit Sleep: „while not Terminated do Sleep(1000)“. Beim Stop kommt das Signal an, aber der Thread reagiert erst nach bis zu 1 Sekunde (oder 30 Sekunden…).
- Blockierende I/O: Datenbank-Calls, HTTP-Requests, Named Pipes, Filesystem-Waits – alles, was „einfach wartet“, ohne auf ein Stop-Signal zu achten.
- Queue-Consumer ohne Wakeup: Ein Worker wartet auf eine Queue, aber beim Stop wird er nicht geweckt, um auszusteigen.
- Lock-Reihenfolge/Deadlocks: Beim Stop wird „Cleanup“ gemacht, während andere Threads noch Locks halten. Das tritt gern nur im Stop-Pfad auf, weil die Reihenfolge dort anders ist als im Normalbetrieb.
Der Windows SCM erwartet, dass ein Service auf ein Stop-Kommando zügig reagiert und seinen Status fortlaufend meldet (über SetServiceStatus; Delphi kapselt das in der Service-Komponente). Wenn du zwar ein Stop-Ereignis annimmst, aber deine Threads nicht sauber runterfährst, bleibt der Prozess lebendig – und Windows entscheidet irgendwann, dass es „zu lange dauert“. Das Ergebnis ist dann entweder ein harter Abbruch oder ein Service, der in einer unklaren Zwischenwelt hängenbleibt.
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,
- auch aus blockierenden Wartezuständen heraus Wirkung entfaltet,
- im Stop-Pfad deterministisch ist (keine „vielleicht kommt er irgendwann raus“-Hoffnung),
- eine klare Timeout-Strategie hat.
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:
- SCM-Erwartung: Windows erwartet, dass du im Status SERVICE_STOP_PENDING regelmäßig Fortschritt meldest. Sonst wirkt es so, als hängst du. Delphi kümmert sich teilweise darum, aber sobald du selbst länger blockierst, brauchst du eine Strategie, wie du weiter Status-Updates ermöglichst (oder wie du deine Stop-Phase kurz hältst).
- Dein eigener Shutdown-Timeout: Du definierst z. B. „Wir geben uns 20 Sekunden, um laufende Jobs sauber abzuschließen, dann brechen wir ab.“ Das ist ein Architekturentscheid: Datenkonsistenz vs. Reboot-Zwang vs. Betriebsvorgaben.
Praktisch heißt das: Dein Service sollte schnell in einen Zustand kommen, in dem er keine neuen Arbeitseinheiten mehr startet, und dann nur noch wartet, dass laufende Arbeit endet – aber nicht unendlich. Und diese Wartephase sollte in kleinen Intervallen laufen, damit du reagieren und ggf. loggen kannst.
Wie lang darf der Stop dauern?
Es gibt keine magische Zahl, die immer passt. Für viele Business-Services ist ein Zielbereich von 5–30 Sekunden realistisch: genug Zeit für „in-flight“ Daten, aber kurz genug für Patch-Fenster. Wenn du regelmäßig länger brauchst, ist das oft ein Hinweis, dass du zu große Einheiten am Stück abarbeitest oder dass externe Abhängigkeiten (DB/HTTP) ohne Timeout laufen.
Implementierung mit TEvent: Aufbau, der im Betrieb stabil bleibt
Ein bewährter Aufbau im Delphi-Service sieht so aus (ohne Framework-Details auszureizen):
- Ein Stop-Event (TEvent, Manual Reset), das beim Stop gesetzt wird.
- Ein oder mehrere Worker-Threads, die in ihrer Hauptschleife regelmäßig auf Stop prüfen.
- Optional ein Work-Event oder eine Queue, die Arbeit signalisiert. Worker warten dann auf „Work oder Stop“.
- Eine Shutdown-Phase, die Worker „joined“ (also wartet, bis sie beendet sind), aber mit Timeout.
Das Entscheidende ist nicht, ob du TThread, omnithreadlibrary oder einen eigenen Pool nutzt, sondern dass deine Worker nicht „blind“ laufen. Ein Worker-Loop sollte strukturell so aussehen: Warten auf Ereignis(e) → Arbeit in kleinen Chunks → zwischen Chunks Stop prüfen → Ressourcen sauber freigeben.
Fallstrick: Terminate allein reicht nicht
Viele Delphi-Threads werden mit Terminate „abgebrochen“. Das ist aber nur ein Flag. Wenn der Thread gerade in einer blockierenden API steckt, passiert erst einmal nichts. Darum ist ein eigenes Stop-Event so hilfreich: Du kannst es in Warteaufrufe integrieren und gezielt Wakeups auslösen.
Fallstrick: FreeOnTerminate im Service-Kontext
In Services sieht man oft FreeOnTerminate := True. Das kann funktionieren, macht Shutdown aber schwerer zu kontrollieren, weil du dann häufig keine saubere Referenz mehr hast, um auf Thread-Ende zu warten und Fehlerzustände zu protokollieren. Für kontrollierte Stop-Logik ist es meist stabiler, Threads explizit zu besitzen und im Shutdown deterministisch zu warten und zu freigeben.
Blockierende Operationen: So machst du sie stop-fähig
Der knifflige Teil ist nicht das Event selbst, sondern die Stellen, an denen dein Service blockiert. Drei typische Klassen:
1) Sleep/Polling ersetzen: Wait mit Stop-Event
Wenn du periodisch arbeitest („alle 10 Sekunden prüfen“), dann nimm nicht Sleep(10000), sondern warte auf ein Event mit Timeout. Dann kann dein Stop-Event das Warten sofort beenden. Das reduziert Stop-Latenz und verhindert das Gefühl „Service reagiert nicht“.
2) Queue-Consumer: Work-Event + Stop-Event kombinieren
Wenn du eine Producer/Consumer-Architektur hast (z. B. Jobs werden in eine Queue gelegt), brauchst du ein Signal, das Consumer aufweckt. Häufig ist das ein weiteres TEvent (Work available). Der Consumer wartet dann auf zwei Handles: „Work“ oder „Stop“. Beim Stop setzt du das Stop-Event und ggf. auch das Work-Event, damit alle Consumer garantiert aus dem Wait herauskommen.
3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade
Bei Datenbankzugriffen oder HTTP-Calls entscheidet sich, ob dein Service sauber stoppt. Für den Betrieb gilt: Kein Call ohne Timeout. Ein Timeout ist kein Luxus, sondern Voraussetzung für Steuerbarkeit. Zusätzlich solltest du zwischen Retries/Backoff-Phasen immer Stop prüfen. Sonst hast du den Klassiker: „Service stoppt nicht, weil er gerade 10 Retries mit Sleep macht“.
Bei manchen Bibliotheken kannst du Abbrüche explizit triggern (z. B. Query-Abbruch). Wenn das nicht geht, musst du zumindest so konfigurieren, dass Timeouts kurz genug sind, um den Shutdown-Timeout nicht zu sprengen.
Stop Pending korrekt: Status, Logging und Erwartungsmanagement
Wenn ein Service stoppt, ist es aus Betriebssicht wichtig, dass man versteht, wo er hängt. Dafür brauchst du zwei Dinge:
- Log-Marker im Stop-Pfad: „Stop angefordert“, „keine neuen Jobs“, „warte auf Worker“, „Worker X beendet“, „Shutdown fertig“.
- Messbare Zeiten: Wie lange dauert der Stop? Welche Phase frisst Zeit? Hier reicht oft schon ein monotones Zeitmaß wie GetTickCount64 oder TStopwatch (monoton = nicht durch Systemzeitänderungen verzerrt).
Wenn du im Stop-Pfad nur einen einzigen Logeintrag „Stopping…“ schreibst, bleibt das Debugging im Feld ein Ratespiel. Im Service-Betrieb sind Logs häufig das einzige, was du ohne Interaktion bekommst.
Welche Logs sind in Services wirklich hilfreich?
- Service-PID, Startzeit, Version/Build (ohne übertriebenen Overhead).
- Anzahl aktiver Worker, Anzahl in-flight Jobs.
- Aktive externe Abhängigkeiten: „DB-Call läuft“, „HTTP-Request läuft“, „Datei-Flush läuft“ (nur aggregiert, nicht jedes Detail).
- Stop-Timeout erreicht: welche Worker sind noch offen?
Debugging im Feld: reproduzierbar machen statt raten
Die Stop-Probleme treten gern nur in Produktion auf: andere Last, andere Latenzen, andere Rechte, andere Patch-Zeitfenster. Ein paar praxiserprobte Hebel:
Service unter Kontrolle testen
- Stop während aktiver Verarbeitung (nicht im Idle).
- Stop während externer Störung: DB kurz nicht erreichbar, HTTP-Endpunkt langsam, Fileshare weg.
- Stop unmittelbar nach Start (Race-Conditions: Worker noch im Aufbau).
Event Viewer und Service Control Manager Signale
Windows schreibt Dienstereignisse, aber die sind oft grob. Besser ist, wenn dein Service selbst in eine Logdatei oder Windows Event Log schreibt. Wichtig ist dabei: Logging sollte im Stop-Pfad noch funktionieren. Wenn du Logger im Shutdown zu früh freigibst oder Flush blockiert, verlierst du genau die entscheidenden Spuren.
Hängende Threads sichtbar machen
Wenn du wiederholt „Stop Timeout“ siehst, lohnt sich ein Blick auf Thread-Zustände (z. B. per Debugger/Procdump in Testumgebung). Häufig findest du dann einen Thread in einem Wait-Zustand auf einem Handle, das nie signalisiert wird, oder in einem Netzwerk-Call ohne Timeout. Der Fix ist dann selten „noch mehr Sleep“, sondern ein sauberer Abbruchpfad.
Wann lohnt sich der Aufwand wirklich?
Ein minimalistischer Dienst, der nur einen Timer hat und keine externen Abhängigkeiten, kann manchmal „einfach stoppen“. Sobald aber eines davon zutrifft, lohnt sich ein sauberer Graceful Shutdown fast immer:
- Der Service verarbeitet Jobs mit Seiteneffekten (Dateien schreiben, DB-Transaktionen, API-Aufrufe).
- Es gibt mehrere Threads oder einen Pool.
- Der Service hängt an Netzwerkressourcen (DB, REST, Message Broker, Fileshares).
- Der Betrieb fordert planbare Wartungsfenster (Reboots, Updates, Failover).
Der Mehrwert ist nicht „Eleganz“, sondern Betriebssicherheit: weniger harte Prozessabbrüche, weniger inkonsistente Zwischenzustände, weniger manuelle Eingriffe.
Praxis-Fallstricke: Was im Shutdown gern schiefgeht
1) Stop wird gesetzt, aber neue Jobs kommen trotzdem rein
Wenn du eingehende Arbeit annimmst (z. B. per Socket, Datei-Trigger, Timer), musst du im Stop-Pfad zuerst die Annahme neuer Arbeit stoppen: Listener schließen, Timer deaktivieren, Scheduler anhalten. Sonst rennst du dem Ende hinterher, weil immer noch neue Jobs starten.
2) Cleanup blockiert (Flush, Close, Finalize)
„Nur noch schnell alles flushen“ kann im Service-Kontext gefährlich sein, wenn das Ziel (Netzlaufwerk, Remote-Log, DB) gerade hängt. Deshalb: Cleanup ja, aber mit begrenzter Zeit. Notfalls musst du entscheiden, welche Daten du in-memory verlierst, statt den kompletten Stop zu blockieren.
3) Locks und Reihenfolge
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.
Наступний крок
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.
Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.
- Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
- REST, доступ до даних, портали та Rollout не відсуваються на пізніший етап.
- Ви заздалегідь бачите, який шлях є економічно та операційно життєздатним.