Net-Base Магазин

02.08.2026

Windows Сервис у Delphi: Правилно имплементирати Graceful Shutdown уз TEvent и Stop-Timeout

Када Windows сервис при заустављању „заглави“, то ретко буде случајност: најчешће блокирају нити, I/O операције или Sleep-петље без пута за прекид. Овај практични чланак показује како у Delphi помоћу TEvent да имплементираш уредно гашење (graceful shutdown) и како правилно да обрађујеш Stop-Timeouts...

02.08.2026

Од теме часописа до пројектне праксе

Одговарајуће странице услуга и техничке странице за чланак

Jedan Windows Service u Delphi često deluje nespektakularno u svakodnevnom radu: radi u pozadini, obrađuje poslove, zapisuje logove, komunicira sa bazama podataka ili REST-API-jem. Dok neko ne klikne „Zaustavi servis“ – ili ne usledi patch-reboot – i servis se ne zaustavi uredno. Tada konzola za servise minutima prikazuje „Zaustavlja se…“, servis zapne u Stop Pending-statusu, a u najgorem slučaju proces bude nasilno prekinut. Upravo zbog toga vredi tretirati Windows Service in Delphi Graceful Shutdown kao svesno arhitektonsko pitanje: sa jasnim shutdown-signalom, definisanim timeout-ima i nitima koje zaista reaguju.

U ovom tekstu nije reč o internim mehanizmima frameworka, već o praktičnom obrascu: TEvent kao stop-signal (sinhronizacioni objekat blizu kernela iz System.SyncObjs), u kombinaciji sa strategijom Stop-Timeout koja uzima u obzir i Windows Service Control Manager (SCM, odnosno Windows komponentu koja pokreće/zaustavlja servise) kao i tvoje sopstvene worker-niti. Dodatno: tipični ivični slučajevi, pristupi za debugovanje i pitanje kada se dodatna logika zaista isplati.

Windows Service u Delphi — uredno gašenje u praksi

Najčešći uzrok je jednostavan: servis ima barem jednu nit koja je zaglavljena u blokirajućoj operaciji i nema put za prekid. Klasični primeri:

  • Polling-Schleifen mit Sleep: „while not Terminated do Sleep(1000)“. Pri zaustavljanju signal stigne, ali nit reaguje tek nakon do 1 sekunde (ili 30 sekundi…).
  • Blockierende I/O: pozivi ka bazama podataka, HTTP-requests, Named Pipes, čekanja na fajl-sistem – sve što se „samo čeka“, bez obraćanja pažnje na stop-signal.
  • Queue-Consumer ohne Wakeup: worker čeka na red, ali pri stopu nije probuđen da izađe.
  • Lock-Reihenfolge/Deadlocks: pri zaustavljanju se radi „cleanup“ dok druge niti još drže lockove. To se rado dešava samo na putanji zaustavljanja jer je redosled tamo drugačiji nego u normalnom radu.

Windows SCM očekuje da servis brzo reaguje na komandu za zaustavljanje i da kontinuirano izveštava o svom statusu (preko SetServiceStatus; Delphi to enkapsulira u service-komponenti). Ako prihvatiš stop-događaj, ali svoje niti ne ugasiš uredno, proces ostaje živ – i Windows će na kraju odlučiti da „predugo traje“. Rezultat je ili grub prekid ili servis koji zapne u nejasnom međustanju.

Osnovno načelo: signal za zaustavljanje koji svaka radna nit razume

Abstrakte Grafik: Worker-Threads warten auf Work oder Stop-Event und beenden geordnet
Kada radnici čekaju na „Work ili Stop“, latencija zaustavljanja opada bez polling-a.

Ein Graceful Shutdown funktioniert nur, wenn du ein Signal hast, das:

  • da ga posmatraju sve relevantne niti,
  • такође из блокирајућих стања чекања делује,
  • у стоп-путањи детерминистички је (нема наде „можда ће некad изаћи“),
  • има јасну Timeout-стратегију.

У Delphi је TEvent за то врло употребљив алат: објекат догађаја који је интерно реализован преко Windows-handle-ова (упоредиво са CreateEvent/SetEvent). Можеш га користити као сигнал „Stop requested“. Сваки worker тада не чека сляпо, већ чека „на посао или на Stop“.

TEvent: правилно бирање — ManualReset vs. AutoReset

Код стоп-сигнала обично желиш Manual Reset (ручно ресетовање): Једном постављено, Event остаје „signaled“ док га не ресетујеш. Овим је обезбеђено да сваки thread који касније уђе у фазу чекања ипак препозна Stop-сигнал. Auto Reset би овде био ризичан, јер после једног чекајућег threada аутоматски поништава сигнал и други thread-ови би могли пропустити Stop-сигнал.

Delphi-Service животни циклус: где Stop заиста стиже

Један Delphi-Windows- и Linux-Services обично се базира на TService (VCL/RTL). SCM шаље команде (Start, Stop, Pause, Continue). Delphi затим позива одговарајуће event-е/методе (у зависности од template-а, нпр. OnStart, OnStop, OnExecute).

Важно за архитектуру:

  • OnStop није место за дуго чекање без ажурирања статуса. То је место где ти покрећеш shutdown и онда контролисано чекаш — са timeout-ом.
  • OnExecute је често петља. Ако тамо радиш „бесконачно“, петља мора реаговати на Stop-сигнал.
  • Worker-Threads (TThread или Thread-Pools) морају реаговати на исти Stop-сигнал, иначе је сервис логички стопиран, али физички још није завршен.

Чист образац: Stop-Event + Join радника + тврд fallback

Практичан образац састоји се из четири корака:

  1. Захтев за Stop: поставити Stop-Event, не прихватати нове послове.
  2. Иницирати wakeup: Ако радници чекају на queue-има или су у sleep-у, морају моћи да се „пробуде“ (нпр. преко Event/Queue-сигнала).
  3. Уређено завршити: радници завршавају своје петље, затварају ресурсе (DB-везе, фајлове, handle-ове) и пријављују „завршено“.
  4. Timeout и fallback: Ако се све не заврши на време, мораш донети одлуку: наставити чекати (са ажурирањем статуса) или контролисано прекинути/тврдо завршити (у зависности од ризика).

Језгро је: ниједна нит не сме искључиво да чека по времену (Sleep) или искључиво да блокира на I/O, без паралелне провере Stop-сигнала. Уместо тога користиш функције чекања које узимају у обзир више сигнала (нпр. „Stop-Event или Work-Event“), или пакујеш I/O у timeout-е плус провере Stop-а.

Правилно размишљање о Stop-timeoutu: SCM-Timeout vs. сопствени Shutdown-Timeout

Овде у пројектима настаје највише неспоразума. Постоје два различита нивоа timeout-а:

  • Očekivanje SCM-a: Windows očekuje da u statusu SERVICE_STOP_PENDING redovno prijavljujete napredak. U suprotnom izgledaće kao da ste zaglavljeni. Delphi se delimično time bavi, ali čim sami duže blokirate, potrebno je imati strategiju kako omogućiti dalja status- ažuriranja (ili kako zadržati vašu stop-fazu kratkom).
  • Vaš sopstveni timeout gašenja: Na primer, definišete „Imamo 20 sekundi da uredno završimo tekuće poslove, posle toga prekidamo.“ To je arhitektonska odluka: konzistentnost podataka vs. prisilni reboot vs. zahtevi rada.

Praktično to znači: servis bi trebalo brzo da pređe u stanje u kome ne pokreće nove jedinice rada, i potom samo čeka da tekući poslovi završe – ali ne beskonačno. I ta faza čekanja treba da radi u kratkim intervalima, kako bi bilo moguće reagovati i po potrebi logovati.

Koliko dugo može trajati zaustavljanje?

Ne postoji magični broj koji uvek odgovara. Za mnoge poslovne servise realan cilj je u opsegu 5–30 sekundi: dovoljno za „in-flight“ podatke, ali dovoljno kratko za patch prozore. Ako redovno treba duže, to je često pokazatelj da obrađujete prevelike jedinice odjednom ili da spoljne zavisnosti (DB/HTTP) nemaju timeout.

Implementacija sa TEvent: arhitektura koja ostaje stabilna u radu

Proverena struktura u Delphi-servisu izgleda ovako (bez razrađivanja detalja frameworka):

  • Jedan Stop-Event (TEvent, Manual Reset), koji se postavlja pri zaustavljanju.
  • Jedan ili više Worker-Threads koji u glavnoj petlji redovno proveravaju Stop.
  • Opcionalno Work-Event ili red (Queue) koji signaliziraju posao. Worker-i tada čekaju na „Work ili Stop“.
  • Faza Shutdown-a koja „join“-uje workere (tj. čeka da završe), ali sa timeout-om.

Bitno nije da li koristite TThread, omnithreadlibrary ili sopstveni pool, već da vaši worker-i ne rade „na slepo“. Worker-loop bi strukturno trebalo da izgleda ovako: čekanje na događaj(e) → rad u malim delovima → između delova proveravati Stop → uredno oslobađanje resursa.

Zamka: samo Terminate nije dovoljno

Mnogi Delphi-threadovi se sa Terminate „prekidaju“. To je međutim samo flag. Ako je thread u tom trenutku u blokirajućem API pozivu, najpre se ništa neće desiti. Zato je sopstveni Stop-Event toliko koristan: može se integrisati u pozive za čekanje i ciljno izazvati wakeup-e.

Zamka: FreeOnTerminate u kontekstu servisa

U servisima se često vidi FreeOnTerminate := True. To može funkcionisati, ali otežava kontrolu gašenja, jer često više nemate čistu referencu da biste čekali kraj threada i beležili greške. Za kontrolisanu stop-logiku obično je stabilnije eksplicitno posedovati threadove i deterministički ih u fazi gašenja čekati i osloboditi.

Blokirajuće operacije: kako ih učiniti zaustavljivim

Scena rešavanja problema: mrežna veza kao uzrok blokirajućih poziva i vremenskih ograničenja
Blokirajući I/O bez timeout‑a je najčešći razlog za zaglavljivanje servisa prilikom zaustavljanja.

Težak deo nije sam događaj, već mesta na kojima se tvoj servis blokira. Tri tipične klase:

1) Sleep/Polling zameniti: Wait sa Stop‑Eventom

Ako servis radi periodično („provera na svake 10 sekundi“), nemoj koristiti Sleep(10000), već čekaj na događaj sa timeout‑om. Tada tvoj Stop‑Event može odmah prekinuti čekanje. To smanjuje latenciju pri zaustavljanju i sprečava osećaj „servis ne reaguje“.

2) Queue‑Consumer: Work‑Event + Stop‑Event kombinovati

Ako imaš Producer/Consumer‑arhitekturu (npr. poslovi se stavljaju u queue), treba ti signal koji budi consumere. Često je to dodatni TEvent (Work available). Consumer tada čeka na dva handle‑a: „Work“ ili „Stop“. Pri Stop‑u postaviš Stop‑Event i po potrebi i Work‑Event, da bi svi consumeri zagarantovano izašli iz Wait‑a.

3) Eksterni pozivi (DB/HTTP): vremenska ograničenja i putevi za prekid

Pri pristupima bazi podataka ili HTTP‑pozivima odlučuje se da li će tvoj servis uredno završiti. Za operacije važi: nijedan poziv bez timeout‑a. Timeout nije luksuz, već preduslov za upravljivost. Dodatno, između pokušaja (Retries)/backoff‑faza uvek proveravaj Stop. Inače imaš klasičan slučaj: „Servis se ne zaustavlja jer trenutno radi 10 retries sa Sleep“.

U nekim bibliotekama možeš eksplicitno pokrenuti prekid (npr. otkazivanje upita). Ako to nije moguće, bar konfigurиши vremenska ograničenja dovoljno kratko da ne prekorače timeout za gašenje.

Stop Pending ispravno: status, logovanje i upravljanje očekivanjima

Merenje vremena i analiza logova za dijagnostiku Stop‑timeoutova kod Windows‑servisa
Logovi po fazama i merenje vremena čine Stop‑timeoutove reprodukovanim i objašnjivim.

Kada se servis zaustavlja, iz operativnog ugla je važno razumeti gde zapinje. Za to ti trebaju dve stvari:

  • Log‑markeri u putanji zaustavljanja: „Zaustavljanje zatraženo“, „nema novih poslova“, „čekam Worker‑e“, „Worker X završen“, „gašenje završeno“.
  • Merenja vremena: Koliko traje zaustavljanje? Koja faza troši vreme? Ovde često dovoljan monotoni merač kao GetTickCount64 ili TStopwatch (monotono = nije izobličeno promenama sistemskog vremena).

Ако у путањи за заустављање забележиш само један лог-унос „Stopping…“, дебаговање у пољу остаје игра нагађања. У раду сервиса логови су често једино што добијаш без интеракције.

Који логови су заиста корисни у сервисима?

  • PID сервиса, време покретања, верзија/build (без претераног оверхеда).
  • Број активних воркера, број послова у току.
  • Активне екстерне зависности: „DB-poziv у току“, „HTTP-захтев у току“, „флашовање датотеке у току“ (само агрегирано, не сваки детаљ).
  • Истек времена за заустављање: који воркери су још активни?

Дебаговање у пољу: учинити репродуцираним уместо нагађања

Проблеми са заустављањем често се јављају само у продукцији: друга оптерећења, друга латенција, друга права, други временски прозори за патчеве. Неколико практично испробаних мера:

Тестирати сервис под контролом

  • Заустављање током активне обраде (не у стању мировања).
  • Заустављање током екстерне ометајуће ситуације: DB кратко недоступна, HTTP крајња тачка споро, дељени фајл-сервер недоступан.
  • Заустављање одмах након покретања (услови трке: воркери још у иницијализацији).

Event Viewer и сигнали Service Control Manager-а

Windows записује догађаје сервиса, али они су често груби. Боље је када сам сервис пише у лог-фајл или Windows Event Log. Важно је: логовање треба да ради и у путу за заустављање. Ако ослободиш логер превише рано у процесу искључења или ако flush блокира, изгубићеш управо оне кључне трагове.

Учини заглављене нити видљивим

Ако поново видиш „Stop Timeout“, вреди погледати стања нити (нпр. преко дебагера/Procdump у тестном окружењу). Често налазиш нит у стању чекања на неком дескриптору/handle-у који никад није сигналиран, или у мрежном позиву без таймаута. Поправка ретко значи „још више sleep-а“, већ чист пут за прекид.

Када се труд заиста исплати?

Минималистички сервис који има само тајмер и нема екстерне зависности понекад може „једноставно да се заустави“. Међутим, чим важи било која од следећих ситуација, чисти Graceful Shutdown се готово увек исплати:

  • Сервис обрађује послове са споредним ефектима (упис датотека, DB транзакције, API позиви).
  • Постоји више нити или пул.
  • Сервис зависи од мрежних ресурса (DB, REST, Message Broker, дељени фајл-сервери).
  • Операција захтева планиране прозоре одржавања (рестартови, ажурирања, failover).

Додао вредност није „елеганција“, већ сигурност у раду: мање оштрих прекида процеса, мање неконзистентних прелазних стања, мање ручних интервенција.

Практичне замке: шта често пође по злу при искључењу

1) Постављен је стоп, али нови послови ипак стижу

Ако прихваташ долазни рад (нпр. преко сокета, тригера на датотеци, тајмера), мораш у путу за заустављање прво зауставити прихватање новог рада: затворити listener, деактивирати тајмер, паузирати scheduler. У супротном ћеш трчати за крајем зато што ће се и даље покретати нови послови.

2) Чишћење блокира (Flush, Close, Finalize)

„Само још брзо све флушовати“ може у контексту сервиса бити опасно ако је циљ (мрежни диск, удаљени лог, DB) тренутно заглављен. Зато: чишћење да, али са ограниченим временом. У крајњем случају мораш одлучити која податка остављаш у меморији, уместо да блокираш целокупно заустављање.

3) Закључавања и редослед

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.

Следећи корак

Када из теме настане реалан пројекат, архитектуру, постојеће стање и операције треба рано разматрати заједно.

Подржавамо не само у појединачним питањима, већ и када из исечака изворног кода, застарелих тема или идеја за портале треба да настане поуздан корпоративни пројекат.

  • Постојеће стање, циљано стање и технички ризици оцењују се заједно.
  • REST, приступ подацима, портали и увођење неће бити одложени за касније фазе.
  • Ви рано увидите који пут је економски и оперативно одржив.

Подели објаву

Поделите ову објаву директно

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта су одмах доступни. За Instagram одмах припремамо линк и кратак текст.

Е-пошта

Инстаграм се отвара у новој картици. Линк и кратак текст се претходно копирају у међуспремник.