Net-Base Magazyn

02.08.2026

Windows usługa w Delphi: prawidłowa implementacja graceful shutdown z użyciem TEvent i Stop-Timeout

Jeśli usługa Windows podczas zatrzymywania „zawiesza się”, rzadko jest to przypadek: zazwyczaj blokują się wątki, operacje I/O lub pętle Sleep bez ścieżki przerwania. Ten artykuł praktyczny pokazuje, jak w Delphi za pomocą TEvent zaimplementować czysty Graceful Shutdown oraz prawidłowo obsługiwać Stop-Timeouts...

02.08.2026

Od tematu magazynowego do praktyki projektowej

Pasujące strony usługowe i techniczne do artykułu

Usługa Windows usługa w Delphi często na co dzień wygląda niespektakularnie: działa w tle, przetwarza zadania, zapisuje logi, komunikuje się z bazami danych lub wywołuje API REST. Aż ktoś kliknie „Zakończ usługę“ — albo zaplanowany jest patch-reboot — i usługa nie zatrzyma się poprawnie. Wówczas konsola usług pokazuje przez minuty „Zamykanie…“, usługa zawiesza się w stanie Stop Pending, a w najgorszym wypadku proces jest brutalnie zabijany. To właśnie tutaj warto potraktować Windows usługa w Delphi łagodne zamykanie jako świadomy temat architektoniczny: z wyraźnym sygnałem zamknięcia, zdefiniowanymi timeoutami i wątkami, które rzeczywiście reagują.

W tym artykule nie chodzi o wewnętrzne mechanizmy frameworków, lecz o praktyczny wzorzec: TEvent jako sygnał Stop (obiekt synchronizacyjny bliski jądru z System.SyncObjs), połączony ze strategią Stop-Timeout, która uwzględnia zarówno Windows Service Control Manager (SCM, czyli komponent Windows uruchamiający/zatrzymujący usługi), jak i twoje własne wątki robocze. Dodatkowo typowe przypadki brzegowe, podejścia do debugowania i kwestia, kiedy dodatkowa logika rzeczywiście ma sens.

Windows usługa w Delphi — łagodne zamykanie w praktyce

Najczęstszą przyczyną jest prosta rzecz: usługa ma co najmniej jeden wątek, który tkwi w operacji blokującej i nie ma ścieżki przerwania. Klasyki:

  • Pętle pollingowe wykorzystujące Sleep: „while not Terminated do Sleep(1000)“. Przy zatrzymaniu sygnał dociera, ale wątek reaguje dopiero po maksymalnie 1 sekundzie (lub 30 sekundach…).
  • Blokujące operacje I/O: wywołania bazy danych, żądania HTTP, Named Pipes, oczekiwania systemu plików — wszystko, co „po prostu czeka“, nie zwracając uwagi na sygnał Stop.
  • Konsument kolejki bez wakeupu: wątek roboczy czeka na kolejkę, ale przy zatrzymaniu nie jest obudzony, by zakończyć działanie.
  • Kolejność blokad / zakleszczenia: Przy zatrzymaniu wykonywany jest „cleanup“, podczas gdy inne wątki nadal trzymają blokady. Zwykle występuje to tylko na ścieżce zatrzymania, ponieważ kolejność tam jest inna niż w normalnym trybie pracy.

Windows SCM oczekuje, że usługa szybko zareaguje na polecenie Stop i będzie na bieżąco raportować swój status (przez SetServiceStatus; Delphi opakowuje to w komponencie serwisu). Jeśli co prawda przyjmujesz zdarzenie Stop, ale nie zamykasz poprawnie swoich wątków, proces pozostaje żywy — i Windows w pewnym momencie uzna, że „to trwa za długo“. Wynikiem jest albo brutalne zakończenie, albo usługa utkwi w nieokreślonym stanie pośrednim.

Zasada podstawowa: sygnał Stop, który rozumie każdy wątek roboczy

Grafika abstrakcyjna: wątki robocze czekają na pracę lub zdarzenie Stop i kończą pracę w uporządkowany sposób
Gdy wątki robocze czekają na „pracę lub zdarzenie Stop”, latencja zatrzymania spada bez pętli pollingowej.

Łagodne zamykanie działa tylko wtedy, gdy masz sygnał, który:

  • może być obserwowany przez wszystkie istotne wątki,
  • działa także z blokujących stanów oczekiwania,
  • w ścieżce zatrzymania jest deterministyczny (bez „może kiedyś się wyjdzie”-nadziei),
  • ma jasną Timeout-Strategie.

W Delphi TEvent jest do tego bardzo użytecznym narzędziem: obiekt zdarzenia, który wewnętrznie realizowany jest przez Windows-uchwyty (porównywalne z CreateEvent/SetEvent). Możesz użyć go jako sygnału „Stop requested”. Każdy Worker nie czeka więc ślepo, lecz czeka „na pracę lub na Stop”.

TEvent właściwy wybór: ManualReset vs. AutoReset

W przypadku sygnałów zatrzymania zwykle chcesz Manual Reset (ręcznie zerowalny): raz ustawione, zdarzenie pozostaje „signaled”, aż je zresetujesz. Dzięki temu każdy wątek, który później wejdzie w fazę oczekiwania, i tak rozpozna sygnał zatrzymania. Auto Reset byłby tutaj ryzykowny, ponieważ automatycznie zeruje sygnał po jednym oczekującym wątku i inne wątki mogłyby przegapić sygnał zatrzymania.

Delphi-cykl życia serwisu: gdzie zatrzymanie naprawdę dociera

Delphi-Windows- und Linux-Services opierają się typowo na TService (VCL/RTL). SCM wysyła polecenia (Start, Stop, Pause, Continue). Delphi wywołuje wtedy odpowiednie zdarzenia/metody (w zależności od szablonu np. OnStart, OnStop, OnExecute).

Ważne dla architektury:

  • OnStop nie jest miejscem do długiego oczekiwania bez aktualizacji statusu. To miejsce, w którym inicjujesz shutdown i następnie kontrolowanie oczekujesz – z Timeout.
  • OnExecute jest często pętlą. Jeśli tam pracujesz „bez końca”, pętla musi reagować na sygnał zatrzymania.
  • Worker-Threads (TThread lub pule wątków) muszą reagować na ten sam sygnał zatrzymania, inaczej serwis będzie logicznie zatrzymany, ale fizycznie wciąż nie zakończony.

Czysty wzorzec: Stop-Event + join workerów + twardy Fallback

Praktyczny wzorzec składa się z czterech kroków:

  1. Zażądanie Stop: ustawić Stop-Event, nie przyjmować nowych zadań.
  2. Wybudzanie: jeśli Worker oczekują na kolejki lub są w Sleep, muszą móc zostać „obudzeni” (np. przez sygnał Event/Queue).
  3. Ukończenie w porządku: Worker kończą swoje pętle, zamykają zasoby (połączenia DB, pliki, uchwyty) i zgłaszają „zakończone”.
  4. Timeout i Fallback: jeśli wszystko nie zakończy się na czas, musisz podjąć decyzję: dalej czekać (z aktualizacją statusu) lub kontrolowanie przerwać/twardo zakończyć (w zależności od ryzyka).

Istota jest taka: żaden wątek nie powinien wyłącznie czekać na podstawie czasu (Sleep) ani wyłącznie blokować się na I/O, bez równoległego uwzględnienia sygnału Stop. Zamiast tego stosujesz funkcje oczekiwania, które uwzględniają wiele sygnałów (np. „Stop-Event oder Work-Event”), albo kapsułujesz I/O w timeouty plus sprawdzenia Stop.

Stop-Timeout właściwie rozumiane: SCM-Timeout vs. własny Shutdown-Timeout

Tutaj w projektach powstaje najwięcej nieporozumień. Istnieją dwa różne poziomy timeoutu:

  • Oczekiwanie SCM: Windows oczekuje, że w stanie SERVICE_STOP_PENDING będziesz regularnie zgłaszać postęp. W przeciwnym razie wygląda to, jakbyś zawiesił się. Delphi częściowo się tym zajmuje, ale gdy sam zablokujesz się na dłużej, potrzebujesz strategii, która pozwoli na dalsze aktualizacje statusu (lub która skróci twoją fazę zatrzymania).
  • Twój własny timeout zamknięcia: Definiujesz np. „Dajemy sobie 20 sekund na poprawne dokończenie bieżących zadań, potem przerywamy.” To decyzja architektoniczna: spójność danych vs. wymuszony reboot vs. wymagania operacyjne.

W praktyce to oznacza: twój serwis powinien szybko przejść w stan, w którym nie rozpoczyna już nowych jednostek pracy, a następnie jedynie czeka na zakończenie trwającej pracy — ale nie w nieskończoność. Ta faza oczekiwania powinna przebiegać w krótkich interwałach, abyś mógł reagować i w razie potrzeby logować.

Jak długo może trwać zatrzymanie?

Nie ma magicznej liczby, która pasuje zawsze. Dla wielu usług biznesowych realistyczny zakres to 5–30 sekund: wystarczająco czasu na dane „in-flight”, ale na tyle krótko, by zmieścić się w oknie patchowania. Jeśli regularnie potrzebujesz więcej, często oznacza to, że przetwarzasz zbyt duże jednostki naraz albo że zewnętrzne zależności (DB/HTTP) działają bez timeoutu.

Implementacja z TEvent: struktura stabilna w eksploatacji

Sprawdzona struktura w serwisie Delphi wygląda tak (bez rozwlekania szczegółów frameworka):

  • Jedno Stop-Event (TEvent, Manual Reset), ustawiane przy zatrzymaniu.
  • Jeden lub kilka Worker-Threads, które w swojej głównej pętli regularnie sprawdzają Stop.
  • Opcjonalnie Work-Event lub kolejka sygnalizująca pracę. Worker wtedy czekają na „Work lub Stop”.
  • Faza zamknięcia, która „joinuje” workery (czyli czeka, aż się zakończą), ale z timeoutem.

Kluczowe nie jest, czy używasz TThread, omnithreadlibrary czy własnego poolu, lecz to, by twoje workery nie działały „na ślepo”. Pętla workera powinna strukturalnie wyglądać tak: oczekiwanie na zdarzenie(a) → praca w małych kawałkach → między kawałkami sprawdzanie Stop → czyste zwalnianie zasobów.

Pułapka: samo Terminate nie wystarczy

Wiele wątków Delphi jest przerywanych przez Terminate. To jednak tylko flaga. Jeśli wątek znajduje się obecnie w blokującej API, początkowo nic się nie stanie. Dlatego własne Stop-Event jest tak pomocne: możesz je włączyć do wywołań oczekujących i wymusić celowe wakeupy.

Pułapka: FreeOnTerminate w kontekście serwisu

W serwisach często widuje się FreeOnTerminate := True. Może to działać, jednak utrudnia kontrolę nad zamykaniem, ponieważ często nie masz już czystej referencji, by czekać na zakończenie wątku i protokołować stany błędów. Dla kontrolowanej logiki zatrzymania zwykle stabilniejsze jest posiadanie wątków i deterministyczne oczekiwanie na nie oraz ich zwalnianie podczas shutdownu.

Operacje blokujące: jak uczynić je podatnymi na zatrzymanie

Troubleshooting-Szene: Netzwerkverbindung als Ursache für blockierende Calls und Timeouts
Blokujące I/O bez limitu czasu to najczęstsza przyczyna zawieszeń podczas zatrzymywania usługi.

Trudny fragment to nie samo zdarzenie, lecz miejsca, w których twoja usługa się blokuje. Trzy typowe klasy:

1) Zastąp Sleep/Polling: oczekiwanie na zdarzenie Stop

Jeśli pracujesz cyklicznie („sprawdzaj co 10 sekund”), nie używaj Sleep(10000), lecz oczekuj na zdarzenie z limitem czasu. Wtedy zdarzenie Stop może natychmiast przerwać oczekiwanie. To zmniejsza opóźnienie zatrzymania i zapobiega wrażeniu „usługa nie odpowiada”.

2) Konsument kolejki: połączenie Work-Event i Stop-Event

Jeśli stosujesz architekturę producent/konsument (np. zadania umieszczane są w kolejce), potrzebujesz sygnału, który wybudzi konsumentów. Często jest to dodatkowy TEvent (Work available). Konsument oczekuje wtedy na dwa uchwyty: „Work” lub „Stop”. Przy Stop ustawiasz Stop-Event i ewentualnie także Work-Event, aby wszyscy konsumenci na pewno opuścili oczekiwanie.

3) Zewnętrzne wywołania (DB/HTTP): limity czasu i ścieżki przerwania

Przy dostępie do bazy danych lub wywołaniach HTTP decyduje się, czy usługa poprawnie się zatrzyma. W eksploatacji obowiązuje: Żadne wywołanie bez limitu czasu. Limit czasu to nie luksus, lecz warunek sterowalności. Dodatkowo powinieneś pomiędzy fazami ponawiania/odstępu zawsze sprawdzać Stop. W przeciwnym razie masz klasyk: „usługa się nie zatrzymuje, bo wykonuje właśnie 10 ponowień z Sleep”.

W niektórych bibliotekach można wywołać przerwanie wprost (np. przerwanie zapytania). Jeśli to niemożliwe, musisz przynajmniej skonfigurować limity czasu na tyle krótko, aby nie przekroczyć czasu oczekiwania na zamknięcie.

Stop Pending poprawnie: Status, Logging und Erwartungsmanagement

Zeitmessung und Log-Analyse zur Diagnose von Stop-Timeouts bei Windows-Services
Logi faz i pomiary czasu czynią timeouty zatrzymywania odtwarzalnymi i wyjaśnialnymi.

Gdy usługa zatrzymuje się, z punktu widzenia eksploatacji ważne jest, aby rozumieć, gdzie się zawiesza. Potrzebujesz do tego dwóch rzeczy:

  • Log-Marker w ścieżce zatrzymywania: „Zatrzymanie żądane”, „brak nowych zadań”, „oczekiwanie na workerów”, „Worker X zakończony”, „zamknięcie ukończone”.
  • Mierzalne czasy: Ile trwa zatrzymanie? Która faza pochłania czas? Często wystarczy monotoniczny pomiar czasu, taki jak GetTickCount64 lub TStopwatch (monotoniczny = niezniekształcany przez zmiany czasu systemowego).

Jeśli w ścieżce zatrzymania zapisujesz tylko jeden wpis logu „Stopping…“, debugowanie w terenie pozostaje zgadywanką. W pracy usługowej logi często są jedynym, co otrzymujesz bez interakcji.

Które logi są w usługach naprawdę przydatne?

  • PID usługi, czas startu, wersja/build (bez nadmiernego narzutu).
  • Liczba aktywnych workerów, liczba zadań w locie.
  • Aktywne zależności zewnętrzne: „DB-Call läuft“, „HTTP-Request läuft“, „Datei-Flush läuft“ (tylko zagregowane, nie każdy drobiazg).
  • Osiągnięto Stop-Timeout: które workery są wciąż aktywne?

Debugowanie w środowisku produkcyjnym: uczynić reprodukowalnym zamiast zgadywać

Problemy ze stopem często występują tylko w produkcji: inne obciążenie, inne opóźnienia, inne uprawnienia, inne okna wdrażania poprawek. Kilka praktycznych, sprawdzonych metod:

Testowanie usługi pod kontrolą

  • Zatrzymanie podczas aktywnego przetwarzania (nie w stanie bezczynności).
  • Zatrzymanie podczas zewnętrznej awarii: DB krótko niedostępna, punkt końcowy HTTP wolny, udział plików niedostępny.
  • Zatrzymanie bezpośrednio po starcie (race conditions: workery nadal w trakcie inicjalizacji).

Podgląd zdarzeń i sygnały Service Control Managera

Windows zapisuje zdarzenia usługowe, ale są one często zgrubne. Lepiej, gdy twój serwis sam zapisuje do pliku logu lub do Windows Event Log. Ważne przy tym: logowanie powinno nadal działać w ścieżce zatrzymania. Jeśli zwalniasz logger zbyt wcześnie podczas shutdownu lub flush jest zablokowany, tracisz dokładnie te decydujące ślady.

Ujawnianie zawieszonych wątków

Jeśli wielokrotnie widzisz „Stop Timeout“, warto przyjrzeć się stanom wątków (np. przez debugger/procdump w środowisku testowym). Często znajdziesz wątek w stanie oczekiwania na uchwyt, który nigdy nie jest sygnalizowany, albo w wywołaniu sieciowym bez timeoutu. Naprawa rzadko polega na „jeszcze większym sleepie“, a raczej na czystej ścieżce przerywania.

Kiedy wysiłek naprawdę się opłaca?

Minimalistyczna usługa, która ma tylko timer i nie ma zależności zewnętrznych, czasami może „po prostu się zatrzymać“. Gdy jednak zachodzi którekolwiek z poniższych, porządny Graceful Shutdown niemal zawsze się opłaca:

  • Usługa przetwarza zadania z efektami ubocznymi (zapis plików, transakcje DB, wywołania API).
  • Występuje wiele wątków lub pula wątków.
  • Usługa zależy od zasobów sieciowych (DB, REST, broker wiadomości, udziały plików).
  • Operacje wymagają planowalnych okien konserwacyjnych (rebooty, aktualizacje, failover).

Wartość dodana to nie „elegancja“, lecz bezpieczeństwo operacyjne: mniej nagłych zakończeń procesów, mniej niespójnych stanów pośrednich, mniej ręcznych interwencji.

Pułapki w praktyce: co często idzie nie tak podczas zamykania

1) Stop jest ustawiony, ale nowe zadania i tak przychodzą

Jeśli przyjmujesz pracę przychodzącą (np. przez socket, trigger plikowy, timer), w ścieżce zatrzymania najpierw musisz zatrzymać przyjmowanie nowej pracy: zamknąć listener, wyłączyć timery, zatrzymać scheduler. W przeciwnym razie będziesz gonić koniec, bo wciąż będą uruchamiać się nowe zadania.

2) Cleanup blokuje się (Flush, Close, Finalize)

„Jeszcze tylko szybko wszystko zflushować“ może być niebezpieczne w kontekście usługi, jeśli cel (dysk sieciowy, zdalny log, DB) właśnie się zawiesza. Dlatego: sprzątanie tak, ale z ograniczonym czasem. W razie potrzeby musisz zdecydować, które dane możesz stracić w pamięci zamiast blokować cały stop.

3) Blokady i kolejność

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

W praktyce Stop może być wywołany wielokrotnie (np. Stop + Shutdown albo Stop pojawia się ponownie). Twoja ścieżka Stop powinna być idempotentna: ustawienie Stop-Event jest dopuszczalne, ale podwójna logika Join/Free musi być odpowiednio zabezpieczona (np. za pomocą flagi atomowej).

Operativer Blick: Was Admins und IT-Leads vom Service erwarten

Dla operacji i administracji ostatecznie nie liczy się, jak „ładny“ jest kod, lecz czy usługa:

  • 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.

To także powód, dlaczego temat Stop-Timeout nie jest wyłącznie „sprawą programistów“: wpływa na cykle patchowania, czasy odzyskiwania i na pytanie, czy automatyczne wdrożenia w ogóle są możliwe.

Konkrete Leitplanken für ein robustes Shutdown-Design

Jeśli chcesz pragmatycznie ustandaryzować ten obszar, sprawdziły się następujące wytyczne:

  • 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

Zawieszający się Stop rzadko jest pojedynczym błędem – zwykle to luka w architekturze: praca odbywa się w wątkach lub blokujących wywołaniach, które nie znają wspólnego sygnału Stop. Z wyraźnym Stop-Event (TEvent, Manual Reset), oczekiwaniami obsługującymi Stop zamiast Sleep, konsekwentnymi timeoutami dla zewnętrznych zależności oraz zdefiniowanym Shutdown-Timeout otrzymujesz usługę, która w codziennej eksploatacji jest przewidywalna.

Ten kod opłaca się szczególnie, jeśli twoja usługa działa w środowiskach produkcyjnych z oknami konserwacyjnymi, zautomatyzowanymi wdrożeniami lub krytycznymi efektami ubocznymi. Wtedy „Graceful Shutdown” to nie kosmetyka, lecz element stabilnej eksploatacji i mniejszej liczby eskalacji przy następnym ponownym uruchomieniu.

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.

Omów projekt lub przedsięwzięcie modernizacyjne z Net-Base.

Następny krok

Jeżeli temat stanie się rzeczywistym projektem, architektura, stan istniejący i eksploatacja powinny być rozpatrywane razem na wczesnym etapie.

Wspieramy nie tylko w pojedynczych zagadnieniach, lecz także wtedy, gdy z fragmentów kodu źródłowego, kwestii związanych z systemami legacy lub koncepcji portalu ma powstać solidny projekt dla przedsiębiorstwa.

  • Stan istniejący, obraz docelowy i ryzyka techniczne są oceniane łącznie.
  • REST, dostęp do danych, portale i Rollout nie będą przesuwane na później.
  • Wcześnie widzą Państwo, która droga jest ekonomicznie i operacyjnie wykonalna.

Udostępnij wpis

Udostępnij ten wpis bezpośrednio

LinkedIn, X, XING, Facebook, WhatsApp i e-mail są natychmiast dostępne. Dla Instagrama przygotowujemy bezpośrednio link i krótki tekst.

E-mail

Instagram otwiera się w nowej karcie. Link i krótki tekst są wcześniej kopiowane do schowka.