Frå magasinetema til prosjektpraksis
Passande teneste- og tekniske sider til innlegget
Ein Windows Service in Delphi verkar ofte lite spektakulær i kvardagen: køyrer i bakgrunnen, handterer jobbar, skriv loggar, snakkar med databasar eller REST-APIs. Til nokon klikkar „Stopp tenesta“ – eller ein patch-reboot står for tur – og tenesta stoppar ikkje reint. Då viser tenestekonsollen minuttvis «Wird beendet…», tenesta heng i Stop Pending-status, og i verste fall blir prosessen tvangsavslutta. Nett her løyner det seg å handsame Windows Service in Delphi Graceful Shutdown som eit medvite arkitekturtema: med eit klart shutdown-signal, definerte timeouts og trådar som verkeleg reagerer.
I denne artikkelen handlar det ikkje om interne detaljar i rammeverket, men om eit praktisk mønster: TEvent som stoppsignal (eit kjernenært synkroniseringsobjekt frå System.SyncObjs), kombinert med ei Stop-Timeout-strategi som tek omsyn både til Windows Service Control Manager (SCM, altså den Windows-komponenten som startar/stoppar tenester) og dine eigne worker-trådar. I tillegg typiske randtilfelle, debugging-tilnærmingar og spørsmålet om når den ekstra logikken verkeleg løner seg.
Windows Service in Delphi Graceful Shutdown i praksis
Den vanlegaste årsaka er enkel: tenesta har minst éin tråd som sit i ei blokkerande operasjon og kjenner ingen avbrotveg. Klassikarar:
- Polling-sløyfer med Sleep: „while not Terminated do Sleep(1000)“. Ved stopp kjem signalet, men tråden reagerer først etter opptil 1 sekund (eller 30 sekund…).
- Blokkerande I/O: databasekall, HTTP-forespurnader, Named Pipes, filesystem-waitar – alt som berre ventar utan å sjå etter eit stoppsignal.
- Queue-consumer utan wakeup: Ein worker ventar på ei kø, men ved stopp blir han ikkje vekka for å avslutte.
- Låsrekkjefylgje/Deadlocks: Ved stopp blir «Cleanup» kjørt medan andre trådar framleis held lås. Dette skjer gjerne berre i stoppløypa, fordi rekkjefølgja der er annleis enn i normaldrift.
Den Windows SCM forventar at ein teneste reagerer raskt på eit stopp-kommando og løpande rapporterer statusen (via SetServiceStatus; Delphi kapslar dette i service-komponenten). Dersom du aksepterer eit stopp-hending, men ikkje køyrer ned trådane dine reint, held prosessen fram med å vere levande – og Windows avgjer etter ei stund at det «tar for lang tid». Resultatet er anten ein hard avbrot eller ei teneste som sit fast i eit uklart mellomstadium.
Grundprinzip: Ein Stop-Signal, das jeder Worker versteht
Ein Graceful Shutdown fungerer berre dersom du har eit signal som:
- som alle relevante trådar kan observere,
- også frå blokkerande ventetilstandar har effekt,
- i Stop-stien deterministisk er (inga „kanskje kjem han ut ein gong“-håp),
- ein klar Timeout-Strategie har.
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:
I praksis betyr det: Tenesta di bør raskt kome i ein tilstand der ho ikkje startar fleire nye arbeidseiningar, og deretter berre ventar på at pågåande arbeid skal bli ferdig – men ikkje i det uendelege. Og denne ventefasen bør gå i små intervall, slik at du kan reagere og eventuelt logge.
Kor lenge kan stoppet vare?
Det finst ikkje noko magisk tal som alltid passar. For mange business-tenester er eit målområde på 5–30 sekund realistisk: nok tid for «in-flight» data, men kort nok for patch-vindauge. Dersom du jamleg treng lengre tid, er det ofte ei indikasjon på at du behandlar for store einingar om gongen eller at eksterne avhengigheiter (DB/HTTP) køyrer utan timeout.
Implementering med TEvent: Oppbygging som held seg stabil i drift
Ein vellukka oppbygging i Delphi-tenesta ser slik ut (utan å dra ut rammeverksdetaljar):
- Eit Stop-Event (TEvent, Manual Reset) som blir sett ved stopp.
- Ein eller fleire Worker-Threads som i hovudsløyfa si regelmessig sjekkar for stopp.
- Valfritt eit Work-Event eller ein kø som signaliserer arbeid. Worker ventar då på «Work eller Stop».
- Ei Shutdown-fase som «joined» workerane (altså ventar til dei er ferdige), men med timeout.
Det avgjerande er ikkje om du bruker TThread, omnithreadlibrary eller ein eigen pool, men at Worker-ane dine ikkje går «blindt». Ein Worker-loop bør strukturelt sjå slik ut: Vent på hending(ar) → arbeid i små chunkar → mellom chunkane sjekke stopp → frigje ressursar ryddig.
Fallgruve: Terminate aleine er ikkje nok
Mange Delphi-Threads blir med Terminate «avbrotne». Det er likevel berre eit flagg. Dersom tråden sit i ei blokkande API, skjer det i første omgang ingenting. Difor er eit eige Stop-Event så nyttig: Du kan integrere det i ventekall og trigge målretta wakeups.
Fallgruve: FreeOnTerminate i service-kontekst
I tenester ser ein ofte FreeOnTerminate := True. Det kan fungere, men gjer nedstenging vanskelegare å kontrollere, fordi du då ofte ikkje har ei rein referanse å vente på for trådslutt eller for å protokollere feiltilstandar. For kontrollert stopplogikk er det som regel meir stabilt å eige Threads eksplisitt og under shutdown deterministisk vente på dei og frigje dei.
Blokkerande operasjonar: Slik gjer du dei stoppbare
Den vanskelege delen er ikkje sjølve eventet, men stadene der tenesta di blokkerer. Tre typiske kategoriar:
1) Sleep/Polling ersetzen: Wait mit Stop-Event
Hvis du køyrer periodisk arbeid («sjekk kvart 10. sekund»), bruk ikkje Sleep(10000). Vent i staden på eit event med timeout. Då kan Stop-Eventet avslutte ventinga med ein gong. Det reduserer stopp-latens og hindrar inntrykket av at tenesta ikkje svarer.
2) Queue-Consumer: Work-Event + Stop-Event kombinieren
Hvis du har ei Producer/Consumer-arkitektur (t.d. jobbar blir lagt i ei kø), treng du eit signal som vekkjer consumerane. Oft er det eit ekstra TEvent (Work available). Consumeren ventar då på to handles: „Work“ eller „Stop“. Ved stopp sette du Stop-Eventet og eventuelt også Work-Eventet, slik at alle consumerar garantert kjem ut av ventinga.
3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade
Ved databaseaksessar eller HTTP-kall avgjerst det om tenesta di stoppar reint. I drift gjeld: Kein Call ohne Timeout. Ein Timeout er ingen luksus, men føresetnad for styrbarheit. I tillegg bør du mellom retries/backoff-fasar alltid sjekke Stop. Elles får du klassikaren: „Tenesta stoppar ikkje fordi han held på med 10 Retries og Sleep“.
I nokre bibliotek kan du trigge avbrot eksplisitt (t.d. Query-Abbruch). Hvis det ikkje går, må du i det minste konfigurere timeoutane korte nok til at dei ikkje sprenger shutdown-timeouten.
Stop Pending korrekt: Status, Logging und Erwartungsmanagement
Når ei teneste stoppar, er det frå driftssynspunkt viktig å forstå kor ho heng. Til det treng du to ting:
- Log-Marker i stoppprosessen: „Stop forespurt“, „ingen nye Jobs“, „venter på Worker“, „Worker X avslutta“, „Shutdown ferdig“.
- Målbare tider: Kor lenge tek stoppet? Kva fase et tid? Her held ofte eit monotont tidsmål som GetTickCount64 eller TStopwatch (monotont = ikkje påverka av systemklokkeendringar).
Hvis du i stoppstien berre skriv ein einaste loggpost «Stopping…», blir feilsøking i feltet eit gjetteleik. I tenestedrift er loggar ofte det einaste du får utan interaksjon.
Kva for loggar er verkeleg nyttige i tenester?
- Service-PID, starttid, versjon/Build (utan overdrevent overhead).
- Tal aktive Worker, tal på in-flight Jobs.
- Aktive eksterne avhengigheiter: «DB-Call køyrer», «HTTP-Request køyrer», «Datei-Flush køyrer» (berre aggregert, ikkje kvart detalj).
- Stopp-timeout nådd: kva Worker er framleis opne?
Feilsøking i felt: gjera det reproduserbart i staden for å gjette
Stoppproblem oppstår ofte berre i produksjon: annan last, andre latensar, andre rettar, andre patch-tidsvindauge. Nokre praksisprøvde tiltak:
Test tenesta under kontroll
- Stopp under aktiv prosessering (ikkje i idle).
- Stopp under ekstern feil: DB kort tid utilgjengeleg, HTTP-endepunkt tregt, Fileshare borte.
- Stopp rett etter oppstart (Race-Conditions: Worker framleis i oppbygging).
Event Viewer og Service Control Manager-signalar
Windows skriv tenestehendingar, men dei er ofte grove. Endå betre er det om tenesta di sjølv skriv til ei loggfil eller Windows Event Log. Viktig: logging må framleis fungere i stoppstien. Dersom du slepp loggaren for tidleg under shutdown eller om Flush blokkerer, mistar du nett dei avgjerande spora.
Gjer hengande trådar synlege
Når du gjentekne gonger ser «Stop Timeout», lønner det seg å sjå på trådtilstandar (t.d. per Debugger/Procdump i testmiljø). Oft finn du då ein tråd i ein ventetilstand på eit Handle som aldri blir signalisert, eller i ein nettverkskall utan timeout. Fiksen er sjeldan «enda meir Sleep», men ein rein avbrotveg.
Når lønner innsatsen seg verkeleg?
Ein minimalistisk teneste som berre har ein Timer og inga eksterne avhengigheiter kan av og til «einfach stoppen». Men så snart éin av følgjande tilfelle gjeld, lønner det seg nesten alltid med ein ryddig Graceful Shutdown:
- Tjenesta handsamar Jobs mit Seiteneffekten (skrive filer, DB-transaksjonar, API-kall).
- Det finst mehrere Threads eller ein Pool.
- Tjenesta er knytt til Netzwerkressourcen (DB, REST, Message Broker, Fileshares).
- Drift krev planbare Wartungsfenster (Reboots, Updates, Failover).
Gevinsten er ikkje «Eleganz», men driftsikkerheit: færre harde prosessavbrot, færre inkonsistente mellomtilstandar, færre manuelle inngrep.
Praksisfallgruver: kva som ofte går gale ved shutdown
1) Stopp blir sett, men nye Jobs kjem framleis inn
Dersom du tek imot innkomande arbeid (t.d. per Socket, Datei-Trigger, Timer), må du i stoppstien først stoppe Annahme neuer Arbeit: Listener stengjast, Timer deaktivert, Scheduler stogga. Elles spring du etter avslutninga fordi nye Jobs framleis startar.
2) Cleanup blokkerer (Flush, Close, Finalize)
«Berre ein rask flush» kan vere farleg i tenestekontekst viss målet (nettverksdisk, Remote-Log, DB) heng. Difor: Cleanup ja, men med begrensa tid. Ved behov må du avgjere kva data du kan misse i minnet i staden for å blokkere heile stoppen.
3) Lås og rekkefølgje
Ved stopp aksesserer du ofte dei same datastrukturane som Worker (Queues, Caches, States). Dersom stopp-tråden held låsar og deretter ventar på at Worker skal avslutte, medan Worker treng same lås, får du ein stopp-deadlock. Motmiddel: hald låsetida kort, ikkje «vente under lås» i stopp-rutinen, definer klar rekkefølgje.
4) Samtidigheit ved dobbelt stopp
I praksis kan stopp bli trigga fleire gonger (t.d. stopp + shutdown, eller stopp kjem igjen). Stopp-rutinen din bør idempotent vere: å setje eit stopp-event er greitt, men dobbel Join/Free-logikk må vernast forsvarleg (t.d. med eit atomisk flagg).
Operativt perspektiv: kva adminar og IT-leiarar forventar av tenesta
For drift og administrasjon tel ikkje kor «pen» koden er, men om tenesta:
- ved stopp avsluttar påliteleg (planleggbar, utan heng),
- ved stopp ikkje skapar inkonsistente data (t.d. halve filer, opne transaksjonar),
- ved feil leverer brukbare loggar,
- under vedlikehaldsvindauge og deploys er forutsigbar.
Det er også grunnen til at temaet stopp-timeout ikkje berre er «utviklarting»: det påverkar patch-syklusar, gjenopprettingstider og om automatiserte deploys i det heile er mogleg.
Konkrete retningslinjer for eit robust shutdown-design
Om du vil standardisere temaet pragmatisk, har desse retningslinjene vist seg nyttige:
- Eit globalt stopp-event, Manual Reset, oppretta tidleg i livssyklusen til tenesta, frigitt seint.
- Ikkje Sleep i Worker-loopar utan stopp-venleg alternativ (Wait med timeout).
- Alle eksterne kall med timeouts (DB, HTTP, fildelingar). Vel timeouts slik at dei passar innanfor ditt stopp-timeout.
- Stopp-timeout som konfigurasjon (t.d. i INI/Registry), slik at drift kan reagere utan å kompilere på nytt.
- Trinnmodell: fyrst graceful (pågåande jobbar får fullføre), deretter valfritt «soft abort» (ingen nye steg), til slutt hard exit som siste utveg.
- Gode stopp-loggar med fasar og tidsmåling.
Konklusjon: TEvent + stopp-timeout er ikkje luksus, men styrbarheit
Ein hengjande stopp er sjeldan ein enkeltfeil – som regel er det eit arkitekturhol: arbeid køyrer i trådar eller i blokkerande kall som ikkje kjenner eit felles stopp-signal. Med eit tydeleg stopp-event (TEvent, Manual Reset), stopp-venlege Waits i staden for Sleep, konsekvente timeouts for eksterne avhengigheiter og eit definert stopp-timeout får du ei teneste som er forutsigbar i kvardagen.
Koden lønar seg særleg om tenesta di køyrer i produksjonsmiljø med vedlikehaldsvindauge, automatiserte deploys eller kritiske sideeffekt. Då er «Graceful Shutdown» ikkje kosmetikk, men ein byggestein for stabil drift og færre eskalasjonar ved neste omstart.
Om du vil setje opp stopp-rutinen deira skikkeleg, eller gjennomgå ein eksisterande Delphi-teneste for robust shutdown-logikk og driftssikkerheit, er ei teknisk sparringssamtale ofte den raskaste vegen til konkrete tiltak: ta kontakt.
For dette temaet er også Delphi Windows Service og Tevent Delphi viktige. Innlegget plasserer desse aspekta på ein forståeleg måte og syner kva som tel i kvardagen.
neste steg
Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift tidleg saman vurderast.
Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.
- Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
- REST, datatilgang, portalar og utrulling blir ikkje utsett til seinare fasar.
- De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.