Net-Base Revija

02.08.2026

Storitev Windows v Delphi: pravilno implementirati urejeno zaustavljanje z TEvent in Stop-Timeout

Če se Windows-storitev med zaustavljanjem 'zatakne', to redko pomeni naključje: večinoma blokirajo niti, I/O ali Sleep-zanke brez poti za prekinitev. Ta praktični prispevek pokaže, kako v Delphi z uporabo TEvent implementirati čist, urejen postopek zaustavitve (graceful shutdown) in pravilno obravnavati Stop-timeoute...

02.08.2026

Od teme v reviji do projektne prakse

Ustrezne strani storitev in tehnični opisi k prispevku

En Windows Service in Delphi se v praksi pogosto zdi nespektakularen: teče v ozadju, obdeluje naloge, zapisuje loge, komunicira z zbirkami podatkov ali REST-APIji. Dokler kdo ne klikne »Ustavi storitev« – ali ne pride do ponovnega zagona zaradi popravka – in storitev se ne zaustavi čisto. Potem konzola storitev več minut prikazuje »Ustavljanje…«, storitev se zatakne v Stop Pending-statusu, in v najslabšem primeru je proces nasilno prekinjen. Ravno tukaj se izplača obravnavati Windows Service in Delphi Graceful Shutdown kot zavesten arhitekturni problem: z jasnim shutdown-signalom, definiranimi timeouti in nitmi, ki se dejansko odzovejo.

V tem prispevku ne gre za notranjosti ogrodij, temveč za praktični vzorec: TEvent kot stop-signal (sinhronizacijski objekt blizu jedra iz System.SyncObjs), v kombinaciji s Stop-Timeout-strategijo, ki upošteva tako Windows Service Control Manager (SCM, torej komponento Windows, ki storitve zažene/ustavi) kot tudi vaše lastne worker-niti. Poleg tega tipični robni primeri, pristopi za razhroščevanje in vprašanje, kdaj se dodatna logika res izplača.

Windows Service in Delphi Graceful Shutdown v praksi

Pogost vzrok je preprost: storitev ima vsaj eno nit, ki je zataknjena v blokirni operaciji in ne pozna poti prekinitve. Klasični primeri:

  • Polling-zanke s Sleep: „while not Terminated do Sleep(1000)“. Ob ustavitvi signal prispe, vendar se nit odzove šele po do 1 sekundi (ali 30 sekundah…).
  • Blokirajoči I/O: klici v bazo podatkov, HTTP-zahteve, Named Pipes, Filesystem-Waits – vse, kar »preprosto čaka«, ne da bi upoštevalo signal za ustavitev.
  • Queue-Consumer ohne Wakeup: delavec čaka na vrsto, vendar ob ustavitvi ni prebujen, da bi izstopil.
  • Vrstni red zaklepov / deadlocki: ob ustavitvi poteka »Cleanup«, medtem ko druge niti še držijo zaklepe. To se pogosto pojavi le na poti ustavitve, ker je tam vrstni red drugačen kot med normalnim obratovanjem.

Windows SCM pričakuje, da se storitev hitro odzove na ukaz za ustavitev in da nenehno poroča o svojem statusu (prek SetServiceStatus; Delphi to ovije v komponento storitve). Čeprav sprejmete dogodek ustavitve, a svojih niti ne zaprete pravilno, proces ostane živ – in Windows bo slej ko prej odločil, da »traja predolgo«. Rezultat je bodisi trda prekinitev bodisi storitev, ki ostane zataknjena v nejasnem vmesnem stanju.

Osnovno načelo: signal za ustavitev, ki ga razume vsak delavec

Abstrakte Grafik: Worker-Threads warten auf Work oder Stop-Event und beenden geordnet
Če delavci čakajo na »delo ali ustavitev«, se latenca ustavitve zmanjša brez pollinga.

Graceful Shutdown deluje le, če imate signal, ki:

  • da ga lahko opazujejo vse relevantne niti,
  • tudi iz blokirajočih čakalnih stanj deluje,
  • v poti zaustavitve determinističen je (brez upanja »morda bo kdaj prišel ven«),
  • ima jasno strategijo časovnih omejitev.

V Delphi je TEvent za to zelo uporabno orodje: objekt dogodka, ki se interno uresničuje preko Windows-Handles (primerljivo z CreateEvent/SetEvent). Lahko ga uporabiš kot signal »Stop requested«. Vsak Worker potem ne čaka slepo, ampak čaka »na delo ali na Stop«.

Pravilna izbira TEvent: ManualReset vs. AutoReset

Pri stop-signalih običajno želiš Manual Reset (ročno ponastavljivo): enkrat nastavljeno, dogodek ostane »signaled«, dokler ga ne ponastaviš. Tako je zagotovljeno, da vsak Thread, ki kasneje vstopi v čakalno fazo, vseeno zazna stop-signal. Auto Reset bi bil tukaj tvegan, saj po enem čakajočem Threadu samodejno ponastavi signal in druge Thread-e bi lahko zamudile stop-signal.

Življenjski cikel Delphi-servisa: kje Stop dejansko prispe

Ena Delphi-Windows- und Linux-Services običajno temelji na TService (VCL/RTL). SCM pošlje ukaze (Start, Stop, Pause, Continue). Delphi nato kliče ustrezne Events/Metode (odvisno od predloge npr. OnStart, OnStop, OnExecute).

Pomembno za arhitekturo:

  • OnStop ni mesto za dolgo čakanje brez posodobitev stanja. To je kraj, kjer sprožiš zaustavitev poženeš in nato kontrolirano čakaš – z rokom (timeout).
  • OnExecute je pogosto zanka. Če tam delaš »brez konca«, se mora zanka odzivati na Stop-signal.
  • Worker-Threads (TThread ali Thread-Pools) se morajo odzvati na isto Stop-signal, sicer je servis logično ustavljen, a fizično še ni končan.

Ustrezen vzorec: Stop-Event + Join der Worker + trdi Fallback

Praktičen vzorec sestoji iz štirih korakov:

  1. Zahtevaj Stop: nastavi Stop-Event, ne sprejemaj več novih opravil.
  2. Sproži prebujanje: če Worker čakajo na vrstah ali so v Sleep, se morajo lahko »prebuditi« (npr. preko Event/Queue-signala).
  3. Nadzorovano zaključevanje: Worker zaključijo svoje zanke, zaprejo vire (DB-povezave, datoteke, Handles) in prijavijo »končano«.
  4. Timeout in Fallback: če se vse ne konča pravočasno, se moraš odločiti: nadaljevati čakanje (s posodobitvijo stanja) ali kontrolirano prekinitev/prisilno zaustavitev (odvisno od tveganja).

Jedro je: nobena nit ne sme izključno čakati po času (Sleep) ali izključno blokirati na I/O, brez vzporednega upoštevanja Stop-signala. Namesto tega uporabiš čakalne funkcije, ki upoštevajo več signalov (npr. »Stop-Event ali Work-Event«), ali pa ovojiš I/O v timeoute skupaj s kontrolami Stop.

Stop-timeout pravilno razumeti: SCM-Timeout vs. lasten Shutdown-Timeout

Tukaj v projektih nastane največ nesporazumov. Obstajata dve različni ravni timeoutov:

  • Pričakovanje SCM: Windows pričakuje, da v statusu SERVICE_STOP_PENDING redno poročaš o napredku. Drugače deluje, kot da si zataknjen. Delphi se delno s tem ukvarja, vendar ko si sam dlje blokiran, potrebuješ strategijo, kako še naprej omogočiti posodobitve statusa (ali kako svojo fazo zaustavitve skrajšati).
  • Tvoj lasten Shutdown-Timeout: Določiš npr. „Damo si 20 sekund, da tekoče naloge čisto dokončamo, nato prekinemo.“ To je arhitekturna odločitev: konsistentnost podatkov vs. prisila ponovnega zagona vs. operativne zahteve.

V praksi to pomeni: tvoja storitev bi morala hitro preiti v stanje, v katerem ne začne več nove delovne enote, in nato le še počaka, da tekoče delo konča – vendar ne v nedogled. Ta čakalna faza naj poteka v kratkih intervalih, da lahko reagiraš in po potrebi zabeležiš v dnevnik.

Kako dolgo sme trajati zaustavitev?

Ni magične številke, ki bi vedno veljala. Za mnoge poslovne storitve je realno ciljano območje 5–30 sekund: dovolj časa za »in-flight« podatke, a dovolj kratko za okna za popravke. Če redno potrebuješ več, je to pogosto znak, da obdeluješ prevelike enote naenkrat ali da zunanje odvisnosti (DB/HTTP) tečejo brez timeouta.

Izvedba z TEvent: zasnova, ki ostane stabilna v obratovanju

Preizkušena zasnova v Delphi-servisu izgleda takole (brez izčrpavanja podrobnosti frameworka):

  • En Stop-Event (TEvent, Manual Reset), ki se ob zaustavitvi nastavi.
  • Ena ali več Worker-Threads, ki v svoji glavni zanki redno preverjajo stanje Stop.
  • Neobvezno Work-Event ali vrsta (Queue), ki signalizira delo. Worker nato čakajo na „Work ali Stop“.
  • Faza Shutdown, ki Worker „joined“ (torej čaka, da se končajo), vendar z timeoutom.

Ključno ni, ali uporabljaš TThread, omnithreadlibrary ali lasten pool, temveč da tvoji Worker ne tečejo „na slepo“. Zanka Workerja naj strukturno izgleda takole: čakanje na dogodek(e) → delo v majhnih kosih → med kosi preverjanje Stop → čista sprostitev virov.

Pasti: Terminate sam po sebi ni dovolj

Veliko Delphi-threadov se prek Terminate „prekine“. To je vendarle le zastavica. Če je nit ravno v blokirajočem API-klicu, se sprva nič ne zgodi. Zato je lastno Stop-Event tako koristno: lahko ga vključiš v čakalne klice in namensko sprožiš wakeup-e.

Pasti: FreeOnTerminate v kontekstu servisa

V storitvah pogosto vidimo FreeOnTerminate := True. To lahko deluje, vendar oteži kontrolo zaustavitve, ker pogosto nimaš več čiste reference, da bi čakal na konec niti in protokoliral napake. Za kontrolirano logiko zaustavljanja je ponavadi stabilneje, da niti eksplicitno poseduješ in jih ob zaustavitvi deterministično počakaš in sprostiš.

Blokirajoče operacije: kako jih narediš ustavljive

Troubleshooting-Szene: Netzwerkverbindung als Ursache für blockierende Calls und Timeouts
Blokirajoči I/O brez časovne omejitve je najpogostejši vzrok za zagozdenje storitve med zaustavljanjem.

Težavni del ni dogodek sam, temveč mesta, kjer se vaša storitev blokira. Tri tipične skupine:

1) Sleep/Polling nadomestite: Wait z Stop-Event

Če izvajate periodično delo („preveri vsakih 10 sekund“), potem ne uporabljajte Sleep(10000), ampak čakajte na dogodek z časovno omejitvijo. Tako lahko vaše Stop-Event takoj prekine čakanje. To zmanjša latenco pri zaustavljanju in prepreči občutek „storitev ne odgovarja“.

2) Queue-Consumer: kombinirajte Work-Event in Stop-Event

Če imate Producer/Consumer-architekturo (npr. naloge se postavijo v vrsto), potrebujete signal, ki zbudi consumerje. Pogosto je to dodatni TEvent (Work available). Consumer nato čaka na dva Handle: „Work“ ali „Stop“. Ob ustavitvi nastavite Stop-Event in po potrebi tudi Work-Event, da se vsi consumerji zagotovo izvlečejo iz čakanja.

3) Externe Calls (DB/HTTP): časovne omejitve in poti prekinitve

Pri dostopih do podatkovnih baz ali HTTP-klicih se odloči, ali se vaša storitev čisto zaustavi. Za obrat velja: ni klica brez časovne omejitve. Časovna omejitev ni luksuz, temveč predpogoj za obvladljivost. Poleg tega med ponovitvami/Backoff-fazami vedno preverite Stop. Sicer imate klasičen primer: „Storitev se ne ustavi, ker ravno izvaja 10 ponovitev s Sleep“.

Pri nekaterih knjižnicah lahko prekinitve sprožite eksplicitno (npr. prekinitev poizvedbe). Če to ni mogoče, morate vsaj nastaviti časovne omejitve dovolj kratke, da ne prekoračijo časa, namenjenega za shutdown.

Stop Pending pravilno: stanje, logging in upravljanje pričakovanj

Zeitmessung und Log-Analyse zur Diagnose von Stop-Timeouts bei Windows-Services
Fazni zapisi v dnevnikih in merjenje časa naredijo Stop-timeoute ponovljive in razumljive.

Ko se storitev zaustavlja, je z vidika obratovanja pomembno razumeti, kje se zatakne. Za to potrebujete dve stvari:

  • Log-markerji v poti za zaustavitev: „Stop zahtevan“, „ni novih opravil“, „čakam na workerje“, „Worker X končan“, „Shutdown končan“.
  • Merljivi časi: Koliko časa traja zaustavitev? Katera faza porablja čas? Pogosto zadostuje monotono merilo časa, kot je GetTickCount64 ali TStopwatch (monotono = neizkrivljeno zaradi sprememb sistemskega časa).

Če v poti zaustavitve zabeležiš samo en vnos dnevnika „Stopping…“, ostane odpravljanje napak na terenu igra ugibanja. V delovanju storitve so dnevniki pogosto edino, kar dobiš brez interakcije.

Kateri dnevniki so v storitvah res koristni?

  • PID storitve, čas zagona, verzija/build (brez pretiranega Overhead).
  • Število aktivnih Workerjev, število in-flight opravkov.
  • Aktivne zunanje odvisnosti: „DB-Call läuft“, „HTTP-Request läuft“, „Datei-Flush läuft“ (le agregirano, ne vsak detajl).
  • Stop-Timeout dosežen: kateri Workerji so še odprti?

Odpravljanje napak na terenu: reproducibilno namesto ugibanja

Te težave s stopom se rade pojavijo le v produkciji: drugačna obremenitev, drugačne zakasnitve, drugačne pravice, drugačna časovna okna za popravke. Nekaj praxiserprobtenih ukrepov:

Storitve testiraj pod nadzorom

  • Stop med aktivno obdelavo (ne v idle).
  • Stop med zunanjo motnjo: DB kratko nedosegljiva, HTTP-endpoint počasen, Fileshare nedosegljiv.
  • Stop neposredno po zagonu (Race-Conditions: Workerji še v vzpostavitvi).

Event Viewer und Service Control Manager Signale

Windows zapisuje dogodke storitev, vendar so ti pogosto grobi. Bolje je, če tvoja storitev sama piše v log datoteko ali v Windows Event Log. Pomembno: logiranje mora delovati tudi v poti zaustavitve. Če logger v shutdownu sprostiš prehitro ali je flush blokiran, izgubiš prav ključne sledi.

Zaznava zataknjenih niti

Če večkrat vidiš „Stop Timeout“, se splača pogledati stanja niti (npr. z debuggerjem/Procdump v testnem okolju). Pogosto najdeš nit v čakanju na handle, ki nikoli ni signaliziran, ali v omrežnem klicu brez timeouta. Rešitev redko pomeni „še več Sleep“, temveč urejen prehod prekinitve.

Kdaj se trud res izplača?

Minimalistična storitev, ki ima le timer in nima zunanjih odvisnosti, lahko včasih „preprosto ustavi“. Vendar, če velja katera od naslednjih točk, se skoraj vedno izplača implementirati urejen Graceful Shutdown:

  • Storitev obdeluje naloge s stranskimi učinki (pisanje datotek, DB-transakcije, API-klici).
  • Obstaja več niti ali upravljalni pool.
  • Storitev je povezana z omrežnimi viri (DB, REST, Message Broker, Fileshares).
  • Obratovanje zahteva načrtna vzdrževalna okna (rebooti, posodobitve, failover).

Dodana vrednost ni „eleganca“, ampak zanesljivost obratovanja: manj brutalnih prekinitev procesov, manj nekonsistentnih vmesnih stanj, manj ročnih posegov.

Praktične pasti: kaj se pri zaustavljanju pogosto ponesreči

1) Stop je sprožen, a vseeno prihajajo nove naloge

Če sprejemaš dohodno delo (npr. preko socketov, file-triggerja, timerja), moraš v poti zaustavitve najprej ustaviti sprejem novega dela: listener zapreti, timer deaktivirati, scheduler zaustaviti. Drugače boš vedno zaostajal za koncem, ker še vedno začnejo prihajati nove naloge.

2) Cleanup blokira (Flush, Close, Finalize)

„Samo še hitro vse flushen“ je v kontekstu storitve lahko nevarno, če cilj (omrežni disk, oddaljeni log, DB) ravno visi. Zato: cleanup da, ampak z omejenim časom. Po potrebi se moraš odločiti, katere podatke izgubiš v pomnilniku, namesto da blokiraš celoten stop.

3) Locks und Reihenfolge

Pri ustavitvi pogosto uporabljaš iste podatkovne strukture kot workerji (vrstice (Queues), predpomnilniki (Caches), stanja (States)). Če ustavitvena nit drži zaklep in nato čaka na konec workerjev, medtem ko workerji potrebujejo isti zaklep, dobiš deadlock pri ustavitvi. Protimere: čas držanja zaklepov držati kratek, v poti za ustavitev ne »čakati pod zaklepom«, definirati jasno zaporedje.

4) Sočasnost pri dvojni ustavitvi

V praksi se ustavitev lahko sproži večkrat (npr. Stop + Shutdown ali se Stop ponovi). Tvoj ustavitveni potek bi moral biti idempotenten: nastavljanje Stop-evanta je v redu, toda dvojna Join/Free-Logik mora biti zavarovana (npr. z atomsko zastavico).

Operativen pogled: kaj skrbniki in IT-vodje pričakujejo od servisa

Za obratovanje in administracijo na koncu ni najpomembnejše, kako »lep« je koda, ampak ali storitev:

  • pri ustavitvi zanesljivo konča (načrtljivo, brez zablokiranj),
  • pri ustavitvi ne proizvaja nedoslednih podatkov (npr. delne datoteke, odprte transakcije),
  • v primeru napake dostavi uporabne dnevnike,
  • med vzdrževalnimi okni in deployi je predvidljiva.

Zato vprašanje Stop-Timeout ni le »zadevica za razvijalce«: vpliva na cikle popravkov, čase obnove in na to, ali so avtomatizirana nameščanja sploh izvedljiva.

Konkretne smernice za robustno zasnovo shutdowna

Če želiš temo pragmatično standardizirati, so se te smernice izkazale za uporabne:

  • En globalni Stop-Event, Manual Reset, ustvarjen zgodaj v življenjskem ciklu storitve, sproščen pozno.
  • Ni Sleep v zankah workerjev brez alternative, ki podpira ustavitev (Wait z timeoutom).
  • Vsi zunanji klici z timeouti (DB, HTTP, datotečni deli). Timeoute izberi tako, da se prilegajo tvojemu Shutdown-Timeout.
  • Stop-Timeout kot konfiguracija (npr. v INI/Registry), da lahko obratovanje reagira brez rekompilacije.
  • Model stopenj: najprej graceful (tečeče naloge dokončane), nato po potrebi »soft abort« (ne sprožijo se novi koraki), nato prisilni izhod kot zadnja možnost.
  • Dobri Stop-dnevniki z jasno ločenimi fazami in merjenjem časa.

Zaključek: TEvent + Stop-Timeout ni luksuz, temveč obvladljivost

Zablokirana ustavitev redko pomeni posamezno napako – praviloma gre za arhitekturno vrzel: delo teče v nitih ali blokirajočih klicih, ki ne poznajo skupnega signala za ustavitev. Z jasnim Stop-Eventom (TEvent, Manual Reset), z wait-ji, ki podpirajo ustavitev namesto Sleep, doslednimi timeouti za zunanje odvisnosti in definiranem Shutdown-Timeout dobiš storitev, ki je v vsakdanjem delovanju predvidljiva.

Ta koda se posebej izplača, če tvoja storitev teče v produkcijskih okoljih z vzdrževalnimi okni, avtomatiziranimi nameščanji ali kritičnimi stranskimi učinki. Tedaj »Graceful Shutdown« ni kozmetika, ampak gradnik za stabilen obrat in manj eskalacij pri naslednjem ponovnem zagonu.

Če želite vaš ustavitveni potek enkrat pravilno nastaviti ali pregledati obstoječi Delphi-servis glede robustne logike shutdowna in varnosti obratovanja, je tehnični sparrings-klic pogosto najhitrejša pot do jasnih ukrepov: kontakt aufnehmen.

Za to temo so pomembni tudi Delphi Windows Service in Tevent Delphi. Prispevek te vidike jasno razvrsti in pokaže, na kaj je v praksi treba paziti.

Projekt ali modernizacijsko predhodje z Net-Base obravnavati.

naslednji korak

Ko iz teme nastane resničen projekt, je treba arhitekturo, obstoječe sisteme in obratovanje zgodaj obravnavati skupaj.

Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.

  • Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
  • REST, dostop do podatkov, portali in Rollout ne bodo prestavljeni v kasnejše faze.
  • Že zgodaj vidite, katera pot je ekonomsko in operativno vzdržna.

Deli objavo

Deli ta prispevek neposredno

LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram pripravljamo povezavo in kratek tekst.

E-pošta

Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.