Nuo žurnalo temos iki projekto įgyvendinimo
Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui
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 tvarkingo išjungimo 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 sustabdymo laiko limito-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 tvarkingo išjungimo praktikoje
Die häufigste Ursache ist simpel: Der Service hat mindestens einen Thread, der in einer blockierenden Operation steckt und keinen Abbruchpfad kennt. Klassiker:
- Polling ciklai su 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…).
- Blokuojantis 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 tvarkingo išjungimo funktioniert nur, wenn du ein Signal hast, das:
- von allen relevanten Threads beobachtet werden kann,
- taip pat veikia net iš blokuojančių laukimo būsenų,
- sustabdymo kelyje deterministiškas yra (nėra „galbūt jis kažkada išeis“ vilties),
- turi aiškią laiko limitų strategiją.
In Delphi yra TEvent tam labai tinkama priemonė: įvykio objektas, kuris viduje realizuojamas per Windows-Handles (panašiai kaip CreateEvent/SetEvent). Gali jį naudoti kaip „Stop requested“ signalą. Kiekvienas Worker tada nelaukia aklai, o laukia „darbų arba sustabdymo“.
TEvent teisingas pasirinkimas: ManualReset vs. AutoReset
Sustabdymo signalams paprastai norima Manual Reset (rankiniu būdu atstatomas): kartą nustatytas įvykis lieka „signaled“, kol jį atstatysi. Tai užtikrina, kad kiekviena gija, kuri vėliau pereina į laukimo fazę, vis tiek atpažins sustabdymo signalą. Auto Reset čia būtų rizikingas, nes jis automatiškai išvalo signalą po vienos laukiančios gijos ir kitos gijos gali praleisti sustabdymo signalą.
Delphi-paslaugos gyvavimo ciklas: kur sustabdymas iš tikrųjų pasiekia
Delphi-Windows- ir Linux-paslaugos paprastai remiasi TService (VCL/RTL). SCM siunčia komandas (Start, Stop, Pause, Continue). Delphi tada iškviečia atitinkamus Events / metodus (priklausomai nuo šablono, pvz. OnStart, OnStop, OnExecute).
Svarbu architektūrai:
- OnStop nėra vieta ilgai laukti be būsenos atnaujinimų. Tai vieta, kur tu inicijuoji išjungimą ir tada kontroliuotai lauki – su laiko limitu.
- OnExecute dažnai yra ciklas. Jei ten dirbi „neribotai“, ciklas turi reaguoti į sustabdymo signalą.
- Worker-Threads (TThread arba Thread-pool’ai) turi reaguoti į tą patį sustabdymo signalą, kitaip paslauga bus logiškai sustabdyta, bet fiziškai dar nebaigta.
Aiškus modelis: Stop-Event + Worker’ų jungimas (Join) + griežtas atsarginis planas
Praktiškai tinkamas modelis susideda iš keturių žingsnių:
- Sustabdymo užklausa: nustatyti Stop-Event, nepriimti daugiau naujų užduočių.
- Provokuoti pabudimus: jei Worker laukia eilėse arba miega, jie turi sugebėti „pabusti“ (pvz., per Event/Queue signalą).
- Tvarkingai užbaigti: Worker užbaigia savo ciklus, uždaro resursus (DB jungtis, failus, Handles) ir praneša „baigta“.
- Laiko limitas ir atsarginis planas: jei ne viskas baigiasi laiku, turi priimti sprendimą: laukti toliau (su būsenos atnaujinimu) arba kontroliuotai nutraukti / priverstinai užbaigti (priklausomai nuo rizikos).
Esminė idėja: jokia gija negali vien tik laukti laiko (Sleep) ar vien tik blokuotis I/O, neatsižvelgdama į sustabdymo signalą. Vietoje to naudok laukimo funkcijas, kurios gali apdoroti kelis signalus vienu metu (pvz. „Stop-Event arba Work-Event“), arba apgaubk I/O laiko limitais ir sustabdymo patikrinimais.
Teisingas Stop-laiko limitų mąstymas: SCM-Timeout vs. savas išjungimo laiko limitas
Čia projektuose įvyksta daugiausia nesusipratimų. Yra dvi skirtingos laiko limitų lygtys:
- SCM lūkestis: Windows tikisi, kad būsenos SERVICE_STOP_PENDING metu tu reguliariai praneši apie pažangą. Priešingu atveju atrodys, tarsi būtum užstrigęs. Delphi iš dalies tuo rūpinasi, bet jei pats blokuosi ilgesnį laiką, tau reikės strategijos, kaip toliau leisti statuso atnaujinimus (arba kaip sutrumpinti savo sustabdymo fazę).
- Tavo nuosavas išjungimo laiko limitas: Tu apibrėži, pavyzdžiui: „Suteikiame sau 20 sekundžių, kad vykdomus darbus tvarkingai užbaigtume, po to nutraukiame.“ Tai architektūrinis sprendimas: duomenų vientisumas vs. priverstinis perkrovimas vs. eksploatacijos reikalavimai.
Praktiškai tai reiškia: tavo servisas turėtų greitai pereiti į būseną, kurioje jis nebepradeda naujų darbo vienetų, ir tada tik laukti, kol baigsis vykdomas darbas – bet ne amžinai. Ši laukimo fazė turėtų vykti mažais intervalais, kad galėtum reaguoti ir, prireikus, fiksuoti žurnalo įrašus.
Kiek ilgai gali trukti sustabdymas?
Nėra vieno stebuklingo skaičiaus, tinkamo visais atvejais. Daugeliui verslo servisų realistiškas tikslinis intervalas yra 5–30 sekundžių: pakankamai laiko „in-flight“ duomenims, bet pakankamai trumpas pataisų (patch) langams. Jei reguliariai reikia ilgesnio laiko, tai dažnai rodo, kad apdoroji per dideles vienetas iš karto arba kad išorinės priklausomybės (DB/HTTP) veikia be timeout.
Implementierung mit TEvent: Aufbau, der im Betrieb stabil bleibt
Kartais patikrinta struktūra Delphi-servise atrodo taip (nesigilinant į Framework detales):
- Vienas Stop-Event (TEvent, Manual Reset), kuris nustatomas sustabdymo metu.
- Viena arba kelios darbo gijos, kurios pagrindiniame cikle reguliariai tikrina Stop.
- Pasirinktinai Work-Event arba eilė (Queue), signalizuojanti darbą. Gijos tada laukia „Work arba Stop“.
- Išjungimo fazė, kuri laukia, kol worker’ai bus „joined“ (t. y. laukia, kol jie baigs darbą), bet su laiko limitu.
Svarbu ne tai, ar naudojiesi TThread, omnithreadlibrary ar savo pool‘u, o kad tavo workeriai neveiktų „aktyviai aklai“. Darbo gijos ciklas struktūriškai turėtų atrodyti taip: laukti įvykio/ų → atlikti darbą mažomis dalimis → tarp dalių tikrinti Stop → tvarkingai atlaisvinti resursus.
Spąstai: Terminate vien tik nepakanka
Daugelis Delphi-gijų yra „nutraukiamos“ su Terminate. Tačiau tai yra tik žymeklis. Jei gija tuo metu užstrigusi blokuojančiame API kvietime, iš karto nieko neįvyks. Todėl savas Stop-Event yra labai naudingas: jį galima integruoti į laukimo kvietimus ir tikslingai sukelti pažadinimus.
Spąstai: FreeOnTerminate serviso kontekste
Servisuose dažnai matomas FreeOnTerminate := True. Tai gali veikti, bet apsunkina išjungimo kontrolę, nes dažnai netenkama tvarkingos nuorodos, kuria lauktum gijos pabaigos ir protokoluotum klaidų būsenas. Dėl kontroliuojamos stop-logikos dažniau stabilesnis sprendimas yra turėti gijas eksplicitiai valdomas ir išjungimo metu determi-niškai laukti bei atlaisvinti jas.
Blokuojančios operacijos: kaip jas padaryti sustabdomomis
Sudėtingiausia dalis nėra pats įvykis, o vietos, kuriose jūsų paslauga blokuojasi. Trys tipinės klasės:
1) Sleep/Polling ersetzen: Wait mit Stop-Event
Jei dirbate periodiškai („patikrinti kas 10 sekundžių“), nenaudokite Sleep(10000), o laukite įvykio su laiko limitu. Tada jūsų Stop-Event gali nedelsiant nutraukti laukimą. Tai sumažina sustabdymo delsą ir užkerta kelią jausmui „paslauga nereaguoja“.
2) Queue-Consumer: Work-Event + Stop-Event kombinieren
Jei turite Producer/Consumer architektūrą (pvz., darbai įdedami į eilę), jums reikia signalo, kuris pažadintų Consumer. Dažnai tai yra papildomas TEvent (Work available). Consumer tada laukia ant dviejų handles: „Work“ arba „Stop“. Kai stabdote, nustatote Stop-Event ir prireikus taip pat Work-Event, kad visi Consumer užtikrintai išeitų iš laukimo.
3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade
Atliekant duomenų bazės užklausas ar HTTP-kvietimus sprendžiasi, ar jūsų paslauga tvarkingai sustos. Eksploatavimui galioja: Nė vieno kvietimo be laiko limito. Laiko limitas nėra prabanga, o valdomumo prielaida. Be to, tarp pakartotinių bandymų/backoff fazių visada turėtumėte tikrinti Stop. Priešingu atveju gausite klasikinę problemą: „Paslauga nesustoja, nes ji šiuo metu atlieka 10 pakartotinių bandymų su Sleep“.
Kai kurios bibliotekos leidžia tiesiogiai inicijuoti nutraukimus (pvz., užklausos nutraukimas). Jei to padaryti neįmanoma, bent jau turite sukonfigūruoti laiko limitus pakankamai trumpai, kad jie neperžengtų išjungimo laiko limito.
Stop Pending teisingai: būsena, žurnalavimas ir lūkesčių valdymas
Kai paslauga sustoja, iš operacijų pusės svarbu suprasti, kur ji užstringa. Tam jums reikia dviejų dalykų:
- Žurnalo žymos stop-kelyje: „Sustabdymas prašomas“, „nėra naujų užduočių“, „laukiama Worker“, „Worker X baigė darbą“, „išjungimas baigtas“.
- Matuojami laikai: kiek užtrunka sustabdymas? Kuri fazė eikvoja laiką? Dažnai užtenka monotoniško laiko mato, tokio kaip GetTickCount64 arba TStopwatch (monotoniškas = nepažeidžiamas sistemos laiko pakeitimų).
Jei sustabdymo kelyje įraše palieki tik vieną žurnalo įrašą „Stopping…“, gedimų šalinimas lauke lieka spėlionė. Serviso veikime žurnalai dažnai yra vienintelis dalykas, kurį gauni be papildomos sąveikos.
Kokie žurnalai paslaugose iš tikrųjų yra naudingi?
- Service-PID, paleidimo laikas, versija/build (be perteklinio overhead).
- Aktyvių worker’ų skaičius, vykdomų (in-flight) užduočių skaičius.
- Aktyvios išorinės priklausomybės: „DB-Call vyksta“, „HTTP-Request vyksta“, „failo Flush vyksta“ (tik agreguotai, ne kiekviena detalė).
- Pasiekta Stop-timeout: kurie worker’ai vis dar nebaigė darbo?
Gedimų šalinimas lauke: padaryti atkuriamu, o ne spėlioti
Sustabdymo problemos dažnai pasireiškia tik gamybos aplinkoje: kitokia apkrova, kitos latencijos, kitos teisės, kiti pataisų langai. Keletas praktikoje patikrintų svertų:
Paslaugos testavimas kontroliuotai
- Sustabdymas aktyvios apdorojimo metu (ne esant tuščiosios eigos).
- Sustabdymas vykstant išoriniam sutrikimui: DB trumpam nepasiekiama, HTTP galinis taškas lėtas, failų dalijimosi paslauga neprieinama.
- Sustabdymas iškart po paleidimo (race conditions: worker’ai dar kuriami).
Event Viewer ir Service Control Manager signalai
Windows rašo paslaugos įvykius, tačiau jie dažnai būna apibendrinti. Geriau, jei tavo paslauga pati rašo į žurnalo failą arba į Windows Event Log. Svarbu: logavimas turi veikti ir sustabdymo kelyje. Jei per anksti atlaisvinsi logger’į arba Flush bus blokuojamas, prarasi būtent tuos lemiamus pėdsakus.
Padaryti užstrigusius gijas matomomis
Jei kartotinai matai „Stop Timeout“, verta pažiūrėti į gijų būsenas (pvz. per debugger’į/Procdump testinėje aplinkoje). Dažnai rasi giją laukimo būsenoje ant handle’o, kuris niekada nėra signalizuojamas, arba tinklo kvietime be timeout’o. Sprendimas retai būna „dar daugiau Sleep“, greičiau tai tvarkingas nutraukimo kelias.
Kada papildomas darbas iš tikrųjų apsimoka?
Minimalistinė paslauga, turinti tik laikmatį ir neturinti išorinių priklausomybių, kartais gali „tiesiog sustoti“. Tačiau kai tenkinamas bent vienas iš žemiau išvardytų kriterijų, tvarkingas sklandus uždarymas (graceful shutdown) beveik visada atsiperka:
- Paslauga apdoroja darbą su šalutiniu poveikiu (rašymas į bylas, DB transakcijos, API kvietimai).
- Yra kelios gijos arba gijų pool’as.
- Paslauga priklauso nuo tinklo išteklių (DB, REST, Message Broker, failų dalinimosi paslaugos).
- Eksploatacija reikalauja planuojamų priežiūros langų (reboot’ai, atnaujinimai, failover).
Nauda nėra „elegancija“, o eksploatacijos patikimumas: mažiau grubaus proceso nutraukimų, mažiau nekonsistentiškų tarpinės būsenos, mažiau rankinių įsikišimų.
Praktinės kliūtys: kas dažnai nepavyksta per sustabdymą
1) Sustabdymas nustatytas, bet vis tiek ateina nauji darbai
Jei priimi įeinantį darbą (pvz. per socket’ą, bylos trigger’į, laikmatį), sustabdymo kelyje pirmiausia turi nutraukti naujo darbo priėmimą: uždaryti klausytojus, išjungti laikmačius, sustabdyti scheduler’į. Kitaip bėgsi paskui pabaigą, nes vis dar pradedami nauji darbai.
2) Cleanup blokuojamas (Flush, Close, Finalize)
„Tik greitai viską išflush’inti“ gali būti pavojinga paslaugos kontekste, jei tikslas (tinklo diskas, nuotolinis žurnalas, DB) šiuo metu užstringa. Todėl: cleanup taip, bet su ribotu laiku. Prireikus turi nuspręsti, kuriuos duomenis prarasti atmintyje, vietoje to, kad blokuotum visą sustabdymą.
3) Užraktai ir tvarka
Sustabdymo metu dažnai naudojate tas pačias duomenų struktūras kaip ir darbo gijos (eilės, talpyklos, būsenos). Jei sustabdymo gija laiko užraktus ir tada laukia darbo gijų pabaigos, tuo tarpu darbo gijos reikalauja tų pačių užraktų, susidaro sustabdymo deadlockas. Priešnuodis: laikyti užraktų laikymo trukmes trumpas, sustabdymo kelyje ne laukti būnant užrakintam, apibrėžti aiškią užrakinimo tvarką.
4) Lygiagretumas dvigubo sustabdymo atveju
Praktikoje sustabdymas gali būti suveiktas kelis kartus (pvz., sustabdymas + shutdown, arba sustabdymas pasikartoja). Jūsų sustabdymo kelias turėtų būti idempotentiškas: sustabdymo įvykio nustatymas yra gerai, tačiau dviguba Join/Free logika turi būti tinkamai apsaugota (pvz., per atomikinį žymeklį).
Operatyvus požiūris: ko administratoriai ir IT vadovai tikisi iš paslaugos
Veikime ir administravime svarbiausia ne tai, koks „gražus“ kodas, o ar paslauga:
- sustabdymo metu patikimai baigiasi (numatoma, be užstrigimų),
- sustabdymo metu negeneruoja nekonsistentiškų duomenų (pvz., pusės failų, atviros transakcijos),
- klaidos atveju pateikia naudingus žurnalo įrašus,
- priežiūros languose ir diegimuose nuspėjama yra.
Todėl tema „sustabdymo laiko limitas“ nėra vien tik „kūrėjų reikalas“: ji veikia pataisų ciklus, atkūrimo laikus ir sprendimą, ar automatizuoti diegimai apskritai įmanomi.
Konkrečios gairės patikimam išjungimo dizainui
Jei norite pragmatiškai standartizuoti šiuos dalykus, šios gairės pasiteisino:
- Globalus sustabdymo įvykis, rankinis atstatymas, sukuriamas anksti tarnybos gyvavimo cikle, atlaisvinamas vėlai.
- Nenaudoti Sleep darbinėse gijų cikluose be sustabdymui tinkamos alternatyvos (Wait su laiko limitu).
- Visi išoriniai skambučiai su laiko limitais (DB, HTTP, failų dalijimasis). Rinkitės laiko limitus taip, kad jie tilptų į jūsų išjungimo laiko limitą.
- Sustabdymo laiko limitas kaip konfigūracija (pvz., INI/registras), kad eksploatacija galėtų reaguoti be naujo kompiliavimo.
- Pakopinis modelis: pirmiausia graceful (vykdomos užduotys baigiamos), tada pasirinktinai „soft abort“ (naujų žingsnių nevykdyti), galiausiai griežtas išėjimas kaip paskutinė priemonė.
- Geri sustabdymo žurnalai su fazėmis ir laiko matavimu.
Išvada: TEvent + sustabdymo laiko limitas nėra prabanga, o valdomumas
Užstrigęs sustabdymas retai būna pavienė klaida – dažniau tai architektūros spraga: darbas vyksta gijose arba blokuojančiuose skambučiuose, kurie nepripažįsta bendro sustabdymo signalo. Aiškus sustabdymo įvykis (TEvent, rankinis atstatymas), laukimai pajėgūs sustabdyti vietoje Sleep, nuoseklūs laiko limitai išorinėms priklausomybėms ir apibrėžtas išjungimo laiko limitas suteikia paslaugai kasdienį nuspėjamumą.
Toks kodas ypač atsiperka, jei jūsų paslauga veikia gamybos aplinkoje su priežiūros langais, automatizuotais diegimais ar kritiniais šalutiniais efektais. Tuomet „švelnus išjungimas“ nėra kosmetika, o stabilaus veikimo ir mažiau eskalacijų po kito perkrovimo dalis.
Jei norite tvarkingai paruošti savo sustabdymo kelią arba patikrinti esamą Delphi paslaugą dėl patikimos išjungimo logikos ir eksploatacinio saugumo, techninis konsultacinis skambutis dažnai yra greičiausias kelias prie aiškių priemonių: susisiekite.
Šiai temai taip pat yra svarbūs Delphi Windows tarnyba ir Tevent Delphi. Straipsnis aiškiai sudėlioja šiuos aspektus ir parodo, kas svarbu kasdieniame darbe.
Aptarti projektą arba modernizacijos iniciatyvą su Net-Base.
Sekantis žingsnis
Kai iš temos tampa realus projektas, architektūrą, esamą aplinką ir eksploatavimą reikėtų anksti nagrinėti kartu.
Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.
- Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
- REST, duomenų prieiga, portalai ir diegimas nebus atidedami į vėlesnes stadijas.
- Jūs anksti matote, kuris kelias yra ekonomiškai ir įmonės veiklos požiūriu tvarus.