A magazintémától a projektgyakorlatig
A bejegyzéshez tartozó szolgáltatási és technikai oldalak
Egy Windows Service in Delphi a mindennapokban gyakran nem feltűnő: háttérben fut, feldolgozza a feladatokat, írja a logokat, kapcsolódik adatbázisokhoz vagy REST-API-khoz. A gond akkor kezdődik, amikor valaki „Szolgáltatás leállítása” gombra kattint — vagy beütemeznek egy javító újraindítást — és a szolgáltatás nem áll le tisztán. Ekkor a Szolgáltatások konzol percekig „Leállítás…”-t mutat, a szolgáltatás Stop Pending állapotban ragad, és rosszabb esetben a folyamatot keményen leállítják. Pont itt érdemes a Windows Service in Delphi Graceful Shutdown-t tudatos architekturális kérdésként kezelni: egy egyértelmű leállítási jel, meghatározott timeoutok és olyan szálak, amelyek valóban reagálnak.
Ebben a bejegyzésben nem a keretrendszer belső működésével foglalkozunk, hanem egy gyakorlati mintával: TEvent mint leállítási jel (egy kernelközeli szinkronizációs objektum a System.SyncObjs-ból), kombinálva egy Stop-Timeout-stratégiával, amely figyelembe veszi mind a Windows Service Control Managert (SCM, tehát az a Windows-komponens, amely elindítja/leállítja a szolgáltatásokat), mind a saját munkaszálakat. Emellett tipikus szélső esetek, hibakeresési megközelítések és az a kérdés, mikor érdemes ténylegesen a plusz logikát beépíteni.
Windows Service in Delphi Graceful Shutdown a gyakorlatban
A leggyakoribb ok egyszerű: a szolgáltatásnak legalább egy olyan szála van, amely egy blokkoló műveletben ragadt, és nincs megszakítási útvonala. Tipikus esetek:
- Polling ciklusok Sleep-tel: „while not Terminated do Sleep(1000)”. A leállítási jel megérkezik, de a szál csak legfeljebb 1 másodperc (vagy 30 másodperc…) múlva reagál.
- Blokkoló I/O: adatbázis-hívások, HTTP-kérések, Named Pipes, fájlrendszer-várakozások – minden, ami „csak vár”, és nem figyel a leállítási jelre.
- Queue-fogyasztó ébresztés nélkül: egy worker egy sorra vár, de leálláskor nem ébresztik fel kilépéshez.
- Lock-sorrend / Deadlockok: a leállítás során takarítást végeznek, miközben más szálak még tartanak zárakat. Ez gyakran csak a leállítási ágban jelentkezik, mert ott a sorrend eltér a normál működéstől.
Az Windows SCM azt várja el, hogy egy szolgáltatás gyorsan reagáljon egy Stop-parancsra és folyamatosan jelentse az állapotát (a SetServiceStatus híváson keresztül; Delphi ezt beágyazza a Service-komponensbe). Ha ugyan elfogadod a Stop-eseményt, de a szálaidat nem állítod le tisztán, a folyamat életben marad — és az Windows előbb-utóbb úgy dönt, hogy „túl sokáig tart”. Az eredmény vagy egy kemény leállítás, vagy egy szolgáltatás, amely egy bizonytalan köztes állapotban ragad.
Alapelv: egy leállítási jel, amelyet minden munkaszál ért
Egy Graceful Shutdown csak akkor működik, ha van egy jel, amely:
- minden releváns szál által észlelhető,
- még blokkoló várakozási állapotokból is hatást fejt ki,
- a Stop-útvonalon determininsztikus (nem a „talán egyszer kijön”-remény),
- egyértelmű Timeout-Strategie-val rendelkezik.
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.
Gyakorlatilag ez azt jelenti: a szolgáltatásodnak gyorsan olyan állapotba kell kerülnie, hogy ne indítson több új munkaegységet, és utána csak arra várjon, hogy a futó munkák befejeződjenek – de nem végtelen ideig. Ez a várakozási fázis kis időközönként fusson, hogy reagálni tudj és szükség esetén naplózz.
Meddig tarthat a leállás?
Nincs egyetlen varázsszám, ami mindig megfelel. Sok üzleti szolgáltatásnál reális célérték a 5–30 másodperc: elegendő idő az „in-flight” adatokra, de elég rövid a patch-ablakokhoz. Ha rendszeresen ennél hosszabb időre van szükséged, az gyakran arra utal, hogy túl nagy egységeket dolgozol fel egyszerre, vagy hogy külső függőségek (DB/HTTP) időkorlát nélkül futnak.
Megvalósítás TEvent-tel: olyan felépítés, amely üzem közben stabil marad
Egy bevált felépítés a Delphi-szolgáltatásban így fest (anélkül, hogy a keretrendszer részleteit kimerítenénk):
- Egy Stop-Event (TEvent, Manual Reset), amely a leállításkor kerül beállításra.
- Egy vagy több worker-szál, amelyek a főciklusukban rendszeresen ellenőrzik a leállítási jelzést.
- Opcionálisan egy Work-Event vagy egy munka jelzésére szolgáló sor (queue). A worker-ek ekkor „Work vagy Stop”-ra várnak.
- Egy Shutdown-fázis, amely ‚joined‘-olja a worker-eket (azaz vár, amíg befejeződnek), de időkorláttal.
A lényeg nem az, hogy TThread, omnithreadlibrary vagy egy saját poolt használsz-e, hanem hogy a worker-eid ne fussanak „vakon”. A worker-ciklus szerkezetileg így nézzen ki: Várakozás esemény(ek)re → munka kis darabokban → darabok között leállítás ellenőrzése → erőforrások tiszta felszabadítása.
Buktató: Terminate önmagában nem elég
Sok Delphi-szálat Terminate-tel „megszakítanak”. Ez azonban csak egy jelző. Ha a szál épp egy blokkoló API-ban van, először semmi sem történik. Ezért hasznos egy külön Stop-Event: beépítheted várakozó hívásokba és célzott ébresztéseket indíthatsz.
Buktató: FreeOnTerminate a szolgáltatás kontextusában
Szolgáltatásoknál gyakran látni a FreeOnTerminate := True-t. Ez működhet, de nehezebbé teszi a leállítás kontrollálását, mert gyakran nincs többé tiszta referenciád, amivel a szál befejeződésére várhatnál és hibállapotokat naplózhatnál. A kontrollált leállítási logika számára általában stabilabb, ha a szálakat expliciten birtoklod és a leállítás során determinisztikusan vársz rájuk és felszabadítod őket.
Blokkoló műveletek: így teszed őket leállíthatóvá
A trükkös rész nem maga az esemény, hanem azok a helyek, ahol a szolgáltatásod blokkolódik. Három tipikus osztály:
1) Sleep/Polling helyettesítése: várakozás Stop-Event-tel
Ha periodikusan dolgozol („10 másodpercenként ellenőrizni”), ne használj Sleep(10000)-et, hanem várj egy eseményre időtúllépéssel. Így a Stop-Event azonnal megszakíthatja a várakozást. Ez csökkenti a leállítási késleltetést és megakadályozza a „Service reagiert nicht” érzetét.
2) Queue-Consumer: Work-Event + Stop-Event kombinálása
Ha Producer/Consumer-architektúrát használsz (pl. a feladatok egy sorba kerülnek), szükséged van egy jelre, ami felébreszti a consumer-eket. Gyakran ez egy további TEvent (Work available). A consumer ezután két handle-re vár: „Work” vagy „Stop”. Leállításkor állítsd be a Stop-Event-et és szükség esetén a Work-Event-et is, hogy minden consumer garantáltan kilépjen a Wait-ből.
3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade
Adatbázis-hozzáféréseknél vagy HTTP-hívásoknál dől el, hogy a szolgáltatás tisztán leáll-e. Üzemeltetési szempontból: Nincs Call időtúllépés nélkül. Az időtúllépés nem luxus, hanem a kezelhetőség előfeltétele. Ezen felül mindig ellenőrizd a Stop-ot az újrapróbálkozások/Backoff-fázisok között. Ellenkező esetben megkapod a klasszikust: „A szolgáltatás nem áll le, mert éppen 10 retry-t hajt végre Sleep használatával.”
Néhány könyvtárnál explicit módon triggerelheted a megszakításokat (pl. lekérdezés-megszakítás). Ha ez nem lehetséges, legalább konfiguráld úgy az időtúllépéseket, hogy elég rövidek legyenek ahhoz, hogy ne lépjék túl a leállítási időkorlátot.
Stop Pending korrekt: státusz, naplózás és elváráskezelés
Ha egy szolgáltatás leáll, üzemeltetési szempontból fontos, hogy értsd, hol akad el. Ehhez két dolog kell:
- Log-marker a leállítási úton: „Leállítás kérése”, „nincsenek új feladatok”, „várás a worker(ek)re”, „Worker X befejezett”, „leállítás kész”.
- Mérhető idők: Mennyi ideig tart a leállás? Melyik fázis emészti a legtöbb időt? Itt gyakran elegendő egy monoton időmérő, például GetTickCount64 vagy TStopwatch (monoton = a rendszeridő-változások nem torzítják).
Ha a leállítási útvonalon csak egyetlen naplóbejegyzést írsz: „Stopping…“, a terepen a hibakeresés találgatássá válik. Szolgáltatás-üzemeltetésben a naplók gyakran az egyetlen információ, amit interakció nélkül kapsz.
Mely naplók hasznosak valóban a szolgáltatásoknál?
- Service-PID, indítási idő, verzió/Build (túlzott overhead nélkül).
- Aktív worker-ek száma, folyamatban lévő (in-flight) feladatok száma.
- Aktív külső függőségek: „DB-hívás folyamatban“, „HTTP-kérelem folyamatban“, „fájl-flush folyamatban“ (csak aggregáltan, nem minden részlet).
- Stop-timeout elérve: mely worker-ek vannak még nyitva?
Hibakeresés a terepen: reprodukálhatóvá tenni a találgatás helyett
A leállási problémák gyakran csak a termelésben jelentkeznek: más terhelés, más késleltetések, más jogosultságok, más javítási időablakok. Néhány, a gyakorlatban bevált beavatkozás:
Szolgáltatás kontrollált tesztelése
- Leállítás aktív feldolgozás közben (ne tétlen állapotban).
- Leállítás külső zavar idején: DB rövid ideig elérhetetlen, HTTP-végpont lassú, fájlkiszolgáló eltűnik.
- Leállítás közvetlenül indítás után (versenyfeltételek: worker-ek még inicializálás alatt).
Event Viewer und Service Control Manager Signale
Windows ír szolgáltatási eseményeket, de ezek gyakran durvák. Jobb, ha a szolgáltatásod közvetlenül egy naplófájlba vagy a Windows Event Log-ba ír. Fontos: a naplózásnak a leállítási útvonalon is működnie kell. Ha a loggert a leállításkor túl korán felszabadítod, vagy a flush blokkolva van, éppen a döntő nyomokat veszíted el.
Függőben lévő szálak láthatóvá tétele
Ha ismételten „Stop Timeout”-ot látsz, érdemes megnézni a szálak állapotát (pl. debuggerrel/Procdump-pal tesztkörnyezetben). Gyakran találsz egy szálat várakozó állapotban egy olyan handle-en, amit soha nem jeleznek, vagy egy hálózati hívást időkorlát nélkül. A javítás ritkán az, hogy „még több sleep”, hanem egy tiszta leállítási útvonal.
Mikor éri meg ténylegesen a ráfordítás?
Egy minimalista szolgáltatás, amely csak egy timert használ és nincsenek külső függőségei, néha „egyszerűen leáll”. Azonban ha bármelyik alábbi feltétel igaz, egy tiszta Graceful Shutdown szinte mindig megéri:
- A szolgáltatás mellékhatásos feladatokat dolgoz fel (fájlok írása, DB-tranzakciók, API-hívások).
- Vannak több szál vagy egy pool.
- A szolgáltatás hálózati erőforrásoktól függ (DB, REST, Message Broker, fájlkiszolgálók).
- Az üzemeltetés tervezhető karbantartási ablakokat követel (újraindítások, frissítések, failover).
Az értéknövekedés nem az „elegancia”, hanem az üzemeltetési biztonság: kevesebb kemény folyamatleállás, kevesebb inkonzisztens átmeneti állapot, kevesebb manuális beavatkozás.
Gyakorlati buktatók: mi szokott elromlani leállításkor
1) Leállítás kezdeményezve, de új feladatok mégis érkeznek
Ha bejövő munkát fogadsz (pl. socketen, fájl-triggerrel, timerrel), a leállítási útvonalban először a új munkák fogadását kell leállítanod: listener bezárása, timer kikapcsolása, scheduler leállítása. Különben a végét fogod üldözni, mert mindig új feladatok indulnak.
2) Takarítás blokkolva (Flush, Close, Finalize)
„Csak még gyorsan mindent flush-olok” veszélyes lehet szolgáltatási kontextusban, ha a cél (hálózati meghajtó, távoli napló, DB) épp akad. Ezért: cleanup igen, de korlátozott idővel. Szükség esetén el kell döntened, mely adatok vesznek el a memóriában, ahelyett hogy a teljes leállást blokkolnád.
3) Zárolások und Reihenfolge
A leállítás során gyakran ugyanazokra az adatszerkezetekre támaszkodsz, mint a Worker-ek (sorok, gyorsítótárak, állapotok). Ha a leállítási szál zárokat tart, majd a Worker-ek befejezésére vár, miközben a Worker-ek ugyanazt a zárolást igénylik, leállítási holtpont (Stop-Deadlock) alakul ki. Ellenintézkedés: a zárolás megtartási idejét tartsd röviden, a leállítási útvonalon ne várakozz „zár alatt”, és határozz meg egyértelmű sorrendet.
4) Párhuzamosság kettős leállításnál
Gyakorlatban a leállítás többször is kiváltható (pl. Stop + Shutdown, vagy a Stop ismét megtörténik). A leállítási útvonaladnak idempotensnek kell lennie: a leállítási esemény beállítása rendben van, de a duplikált Join/Free-logikát gondosan védeni kell (pl. egy atomikus jelzővel).
Operatív szemszög: mit várnak az adminisztrátorok és az IT-vezetők a szolgáltatástól
Üzemeltetés és adminisztráció szempontjából a végén nem az számít, hogy mennyire „szép” a kód, hanem hogy a szolgáltatás:
- leállításkor megbízhatóan befejeződik (tervezhető, akadások nélkül),
- leállításkor nem hoz létre inkonzisztens adatokat (pl. félbehagyott fájlok, nyitott tranzakciók),
- hiba esetén használható naplókat szolgáltat,
- karbantartási ablakok és telepítések során előrelátható legyen.
Ez az oka annak is, hogy a leállítási időkorlát nem csupán „fejlesztői ügy”: befolyásolja a patch-ciklusokat, a helyreállítási időket és azt, hogy egyáltalán lehetségesek-e az automatizált telepítések.
Konkrét iránymutatások robosztus leállítási tervekhez
Ha pragmatikusan szeretnéd standardizálni a témát, ezek az iránymutatások bizonyultak hasznosnak:
- Egy globális leállítási esemény, Manual Reset, a szolgáltatás életciklusának korai szakaszában létrehozva, későn felszabadítva.
- Ne használj Sleep-et a Worker-ciklusokban Stop-képes alternatíva nélkül (várakozás timeout-tal).
- Minden külső hívás timeout-tal (DB, HTTP, fájlkiszolgálók). Válaszd a timeoutokat úgy, hogy illeszkedjenek a leállítási időkorlátodba.
- Leállítási időkorlát konfigurálható (pl. INI/Registry), hogy az üzemeltetés reagálhasson újrafordítás nélkül.
- Fokozatos modell: először graceful (futó feladatok befejezése), majd opcionális „soft abort” (nincsenek új lépések), végül kemény kilépés végső eszközként.
- Jó leállítási naplók fázisokkal és időméréssel.
Következtetés: TEvent + leállítási időkorlát nem luxus, hanem irányíthatóság
Az elakadt leállítás ritkán egyedi hiba – legtöbbször architekturális hiányosság: munka fut szálakban vagy blokkoló hívásokban, amelyek nem ismerik a közös leállítási jelet. Egy jól definiált leállítási esemény (TEvent, Manual Reset), leállítható várakozások Sleep helyett, következetes timeoutok a külső függőségekre és egy meghatározott leállítási időkorlát mellett olyan szolgáltatást kapsz, amely a mindennapokban előrelátható.
A megoldás különösen megéri, ha a szolgáltatásod éles környezetben, karbantartási ablakokkal, automatizált telepítésekkel vagy kritikus mellékhatásokkal dolgozik. Ilyenkor a „Graceful Shutdown” nem kozmetika, hanem az üzembiztonság építőeleme és kevesebb eszkaláció a következő újraindításnál.
Ha a leállítási útvonalat egyszer tisztán fel akarod építeni, vagy egy meglévő Delphi-szolgáltatást robusztus leállítási logika és üzemeltetési biztonság szempontjából felülvizsgálnál, egy technikai sparring-hívás gyakran a leggyorsabb út a konkrét intézkedésekhez: Lépj kapcsolatba.
Ehhez a témához kapcsolódóan a Delphi Windows szolgáltatás és a Tevent Delphi is fontos. A cikk ezeket az aspektusokat érthetően rendszerezi és bemutatja, mire kell figyelni a gyakorlatban.
Projektet vagy modernizációs feladatot megvitatni a következővel: Net-Base.
Következő lépés
Ha a téma valós projektté válik, az architektúrát, a meglévő rendszert és az üzemeltetést már korán együtt kell értékelni.
Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.
- A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
- REST, az adathozzáférés, a portálok és a Rollout nem kerülnek utólagos teendőkként elhalasztásra.
- Már korán láthatja, melyik út gazdaságilag és üzemeltetési szempontból életképes.