Fra magasinets tema til projektpraksis
Passende service- og tekniske sider til artiklen
En Windows Service in Delphi fremstår ofte uspektakulær i daglig drift: den kører i baggrunden, behandler jobs, skriver logs, taler med databaser eller REST-API’er. Indtil nogen klikker „Stop tjeneste“ – eller et patch-reboot er nødvendigt – og servicen ikke stopper ordentligt. Så viser tjenestekonsollen i flere minutter „Wird beendet…“, servicen hænger i Stop Pending-status, og i værste fald bliver processen tvangslukket. Netop her er det værd at behandle Windows Service in Delphi Graceful Shutdown som et bevidst arkitekturtema: med et klart shutdown-signal, definerede timeouts og tråde, der reelt reagerer.
Dette indlæg handler ikke om framework-interna, men om et praktikbart mønster: TEvent som stop-signal (et synkroniseringsobjekt på kernelniveau fra System.SyncObjs), kombineret med en Stop-Timeout-strategi, der både tager højde for Windows Service Control Manager (SCM, altså den Windows-komponent, der starter/stopper tjenester) og dine egne Worker-Threads. Derudover typiske kanttilfælde, debugging-tilgange og spørgsmålet om, hvornår den ekstra logik reelt er berettiget.
Windows Service in Delphi Graceful Shutdown i praksis
Den hyppigste årsag er simpel: servicen har mindst én tråd, der sidder i en blokerende operation og ikke kender en afbrydelsessti. Typiske eksempler:
- Polling-løkker med Sleep: „while not Terminated do Sleep(1000)“. Ved stop ankommer signalet, men tråden reagerer først efter op til 1 sekund (eller 30 sekunder…).
- Blokerende I/O: databasekald, HTTP-requests, Named Pipes, filesystem-waits – alt, der „bare venter“, uden at tage hensyn til et stop-signal.
- Queue-Consumer uden Wakeup: en worker venter på en kø, men ved stop bliver den ikke vækket for at afslutte.
- Lås-rækkefølge/Deadlocks: ved stop udføres „Cleanup“, mens andre tråde stadig holder låse. Det optræder ofte kun i stop-stien, fordi rækkefølgen der er anderledes end i normal drift.
Windows SCM forventer, at en service reagerer hurtigt på et stop-kommando og løbende rapporterer sin status (via SetServiceStatus; Delphi kapsler det i servicekomponenten). Hvis du godt tager imod en stop-begivenhed, men ikke lukker dine tråde ordentligt ned, forbliver processen i live – og Windows beslutter på et tidspunkt, at det „varer for længe“. Resultatet er enten en hård afbrydelse eller en service, der hænger i en uklar mellemtilstand.
Grundprincip: Et stop-signal, som enhver Worker forstår
Et Graceful Shutdown fungerer kun, hvis du har et signal, som:
- kan observeres af alle relevante tråde,
- auch aus blockierenden ventetilstande heraus virkning får,
- i stop-stien deterministisk er (ingen ”måske kommer den engang ud”-håb),
- en klar timeout-strategi har.
In Delphi er TEvent et meget anvendeligt værktøj til dette: et event-objekt, der internt er implementeret via Windows-handles (sammenligneligt med CreateEvent/SetEvent). Du kan bruge det som et „Stop requested“-signal. Hver worker venter så ikke blot blindt, men venter „på arbejde eller på Stop“.
TEvent richtig wählen: ManualReset vs. AutoReset
Til stop-signaler vil du som regel have Manual Reset (manuel nulstilling): Når det er sat, forbliver eventet „signaleret“, indtil du nulstiller det. Det sikrer, at enhver tråd, der senere går ind i en ventefase, alligevel opdager stop-signalet. Auto Reset ville være risikabelt her, fordi det automatisk nulstiller signalet efter én ventende tråd, og andre tråde kunne misse stop-signalet.
Delphi-Service-Lebenszyklus: Wo Stop wirklich ankommt
En Delphi-Windows- und Linux-Services er typisk baseret på TService (VCL/RTL). SCM sender kommandoer (Start, Stop, Pause, Continue). Delphi kalder så de tilsvarende events/metoder (afhængigt af template f.eks. OnStart, OnStop, OnExecute).
Vigtigt for arkitekturen:
- OnStop er ikke stedet til langvarig venten uden statusopdateringer. Det er stedet, hvor du igangsætter shutdown og derefter venter kontrolleret – med timeout.
- OnExecute er ofte en løkke. Hvis du arbejder der „uendeligt“, skal løkken reagere på et Stop-signal.
- Worker-Threads (TThread eller thread-pools) skal reagere på det samme Stop-signal, ellers er servicen logisk stoppet, men fysisk ikke færdig.
Rent mønster: Stop-Event + Join der Worker + hård fallback
Det praksisegnede mønster består af fire trin:
- Anmode om stop: Sæt Stop-event, modtag ikke flere nye jobs.
- Udløs wakeups: Hvis workere venter på køer eller sover, skal de kunne „vågne“ (f.eks. via event-/kø-signal).
- Luk ordnet ned: Workere afslutter deres løkker, lukker ressourcer (DB-forbindelser, filer, handles) og rapporterer „færdig“.
- Timeout og fallback: Hvis ikke alt afslutter rettidigt, skal du træffe en beslutning: fortsætte med at vente (med statusopdatering) eller afbryde kontrolleret/hardt (afhængig af risiko).
Kernen er: ingen tråd må udelukkende vente på tid (Sleep) eller udelukkende blokere på I/O uden parallelt at tage højde for et Stop-signal. I stedet bruger du ventefunktioner, der tager flere signaler i betragtning (f.eks. „Stop-Event eller Work-Event“), eller du indkapsler I/O i timeouts plus stop-checks.
Stop-timeout richtig denken: SCM-Timeout vs. eigener Shutdown-Timeout
Her opstår de fleste misforståelser i projekter. Der er to forskellige timeout-niveauer:
- SCM-forventning: Windows forventer, at du i status SERVICE_STOP_PENDING regelmæssigt rapporterer fremdrift. Ellers ser det ud, som om du hænger. Delphi tager sig delvist af det, men så snart du selv blokerer længere, har du brug for en strategi for, hvordan du fortsat muliggør statusopdateringer (eller hvordan du holder din stopfase kort).
- Din egen Shutdown-Timeout: Du definerer f.eks. „Vi giver os 20 sekunder til at afslutte kørende jobs ordentligt, derefter afbryder vi.“ Det er et arkitekturvalg: datakonsistens vs. tvungen genstart vs. driftskrav.
Praktisk betyder det: Din service bør hurtigt komme i en tilstand, hvor den ikke starter nye arbejdsenheder mere, og derefter kun venter på, at igangværende arbejde afsluttes – men ikke uendeligt. Og denne ventefase bør køre i små intervaller, så du kan reagere og eventuelt logge.
Hvor længe må stopfasen vare?
Der findes ikke et magisk tal, der altid passer. For mange forretningsservices er et målområde på 5–30 sekunder realistisk: nok tid til igangværende data, men kort nok til patch-vinduer. Hvis du regelmæssigt har brug for længere tid, er det ofte et tegn på, at du behandler for store enheder ad gangen, eller at eksterne afhængigheder (DB/HTTP) kører uden timeout.
Implementering med TEvent: Aufbau, der i drift forbliver stabil
En gennemprøvet opbygning i Delphi-servicen ser sådan ud (uden at gå i dybden med framework-detaljer):
- Et Stop-Event (TEvent, Manual Reset), som sættes ved stop.
- En eller flere Worker-Threads, som i deres hovedsløjfe regelmæssigt tjekker for stop.
- Valgfrit et Work-Event eller en queue, der signalerer arbejde. Worker venter så på „Work eller Stop“.
- En Shutdown-fase, som joined workerne (dvs. venter, til de er afsluttede), men med timeout.
Det afgørende er ikke, om du bruger TThread, omnithreadlibrary eller en egen pool, men at dine workere ikke kører „blindt“. En worker-loop bør strukturelt se sådan ud: vente på begivenhed(er) → arbejde i små chunks → mellem chunks tjekke for stop → frigive ressourcer ordentligt.
Faldgrube: Terminate alene er ikke nok
Mange Delphi-threads bliver med Terminate „afbrudt“. Det er dog kun et flag. Hvis tråden netop sidder i en blokerende API, sker der i første omgang ingenting. Derfor er et eget Stop-Event så nyttigt: Du kan integrere det i ventekald og målrettet vække tråden.
Faldgrube: FreeOnTerminate i service-kontekst
I services ser man ofte FreeOnTerminate := True. Det kan fungere, men gør shutdown sværere at kontrollere, fordi du så ofte ikke har en ren reference til at vente på trådens afslutning og logge fejltilstande. For kontrolleret stop-logik er det som regel mere stabilt at eje trådene eksplicit og deterministisk vente på dem og frigive dem i shutdown.
Blokerende operationer: Så gør du dem stopbare
Det vanskelige er ikke selve eventet, men de steder, hvor din service blokerer. Tre typiske klasser:
1) Erstat Sleep/Polling: Vent på et Stop-Event
Hvis du arbejder periodisk („tjek hvert 10. sekund“), så brug ikke Sleep(10000), men vent på et event med timeout. Så kan dit Stop-Event afslutte ventetiden med det samme. Det reducerer stop-latenstid og forhindrer følelsen „servicen reagerer ikke“.
2) Queue-forbrugere: Kombiner Work-Event + Stop-Event
Hvis du har en producer/consumer-arkitektur (f.eks. jobs, der lægges i en queue), har du brug for et signal, der vækker consumerne. Det er ofte et ekstra TEvent (Work available). Consumeren venter så på to handles: „Work“ eller „Stop“. Ved Stop sætter du Stop-Event og eventuelt også Work-Event, så alle consumerne med garanti kommer ud af ventetilstanden.
3) Eksterne kald (DB/HTTP): Timeouts og afbrudsstier
Ved databaseadgang eller HTTP-kald afgøres det, om din service stopper rent. I drift gælder: Intet kald uden timeout. Et timeout er ikke luksus, men en forudsætning for styrbarhed. Derudover bør du mellem retries/backoff-faser altid tjekke for Stop. Ellers får du klassikeren: „Servicen stopper ikke, fordi den lige nu kører 10 retries med Sleep“.
I nogle biblioteker kan du udløse afbrydelser eksplicit (f.eks. Query-Abbruch). Hvis det ikke er muligt, skal du som minimum konfigurere timeouts korte nok til ikke at overskride shutdown-timeouten.
Stop Pending korrekt: Status, logning og forventningsstyring
Når en service stopper, er det fra driftsmæssigt synspunkt vigtigt at forstå, hvor den hænger. Til det har du brug for to ting:
- Log-markører i stop-stien: „Stop anmodet“, „ingen nye jobs“, „venter på Worker“, „Worker X afsluttet“, „Shutdown færdig“.
- Målbare tider: Hvor lang tid tager stop? Hvilken fase bruger tid? Her er ofte et monotont tidsmål som GetTickCount64 eller TStopwatch tilstrækkeligt (monotont = ikke forvrænget af systemtidsændringer).
Hvis du i stopsekvensen kun skriver en enkelt logpost „Stopping…“, forbliver fejlsøgningen i marken et gætteri. I service-drift er logs ofte det eneste, du får uden interaktion.
Hvilke logs er virkelig nyttige i services?
- Service-PID, starttid, version/build (uden overdreven overhead).
- Antal aktive workere, antal igangværende jobs.
- Aktive eksterne afhængigheder: „DB-Call kører“, „HTTP-Request kører“, „Datei-Flush kører“ (kun aggregeret, ikke hvert enkelt detalje).
- Stop-timeout nået: hvilke workere er stadig åbne?
Fejlsøgning i marken: gør det reproducerbart i stedet for at gætte
Stopproblemer opstår ofte kun i produktion: anden belastning, andre latenser, andre rettigheder, andre patch-tidsvinduer. Et par praksisafprøvede greb:
Test servicen under kontrol
- Stop mens der er aktiv behandling (ikke i idle).
- Stop under ekstern forstyrrelse: DB kortvarigt utilgængelig, HTTP-endpoint langsomt, fileshare væk.
- Stop umiddelbart efter start (race-conditions: workere stadig ved at blive opbygget).
Event Viewer og Service Control Manager-signaler
Windows skriver tjenestebegivenheder, men de er ofte grove. Bedre er, hvis din service selv skriver til en logfil eller Windows Event Log. Vigtigt er: logning skal stadig fungere i stopsekvensen. Hvis du frigiver loggere for tidligt i shutdown eller flush blokeres, mister du netop de afgørende spor.
Gør hængende tråde synlige
Hvis du gentagne gange ser „Stop Timeout“, er det værd at kigge på trådenes tilstande (f.eks. med en debugger/Procdump i en testmiljø). Ofte finder du en tråd i en ventetilstand på et handle, der aldrig bliver signalleret, eller i et netværkskald uden timeout. Løsningen er sjældent „endnu mere sleep“, men en ordentlig afbrydelsessti.
Hvornår er indsatsen virkelig berettiget?
En minimalistisk tjeneste, der kun har en timer og ingen eksterne afhængigheder, kan nogle gange „bare stoppe“. Men så snart en af følgende forhold gælder, er en ordentlig graceful shutdown næsten altid berettiget:
- Tjenesten behandler jobs med sideeffekter (filskrivninger, DB-transaktioner, API-opkald).
- Der er flere tråde eller en pool.
- Tjenesten afhænger af netværksressourcer (DB, REST, Message Broker, Fileshares).
- Drift kræver planlagte vedligeholdelsesvinduer (reboots, opdateringer, failover).
Værdien er ikke „elegance“, men driftssikkerhed: færre hårde procesafbrud, færre inkonsistente mellemliggende tilstande, færre manuelle indgreb.
Praktiske faldgruber: Hvad ofte går galt under shutdown
1) Stop-signalet sættes, men nye jobs kommer stadig ind
Hvis du accepterer indkommende arbejde (f.eks. via socket, fil-trigger, timer), skal du i stopsekvensen først stoppe accepten af nyt arbejde: luk lyttere, deaktiver timere, stop scheduleren. Ellers løber du efter slutningen, fordi nye jobs stadig starter.
2) Cleanup blokerer (flush, close, finalize)
„Bare lige flush alt“ kan i servicekonteksten være farligt, hvis målet (netværksdrev, remote-log, DB) lige hænger. Derfor: cleanup ja, men med begrænset tid. Hvis nødvendigt må du beslutte, hvilke data du mister i hukommelsen, i stedet for at blokere hele stopproceduren.
3) Locks og rækkefølge
Når Stop køres, tilgår du ofte de samme datastrukturer som workerne (Queues, Caches, States). Hvis Stop-threaden holder Locks og derefter venter på, at workerne afslutter, samtidigt med at workerne har brug for samme Lock, får du en Stop-deadlock. Modforholdsregler: hold Lock-hold-tider korte, vent ikke „under Lock“ i Stop-stien, og definer en klar rækkefølge.
4) Samtidighed ved dobbelt Stop
I praksis kan Stop blive trigget flere gange (f.eks. Stop + Shutdown, eller Stop opstår igen). Din Stop-sti bør være idempotent: det er i orden at sætte et Stop-event, men dobbelt Join/Free-logik skal beskyttes korrekt (f.eks. via et atomisk flag).
Operationelt perspektiv: Hvad Admins og IT-Leads forventer af Service
For drift og administration handler det i sidste ende ikke om, hvor „pæn“ koden er, men om tjenesten:
- ved Stop afslutter pålideligt (planmæssigt, uden at hænge),
- ved Stop producerer ingen inkonsistente data (f.eks. halve filer, åbne transaktioner),
- i fejltilfælde leverer brugbare logs,
- ved vedligeholdelsesvinduer og udrulninger er forudsigelig.
Det er også grunden til, at emnet Stop-Timeout ikke kun er „udviklerkram“: det påvirker patch-cyklusser, recovery-tider og spørgsmålet om, hvorvidt automatiserede deployments overhovedet er mulige.
Konkrete retningslinjer for et robust Shutdown-design
Hvis du vil standardisere emnet pragmatisk, har disse retningslinjer vist sig effektive:
- Et globalt Stop-Event, Manual Reset, oprettes tidligt i Service-livscyklussen og frigives sent.
- Ingen Sleep i worker-loops uden en Stop-kompatibel alternativ (Wait med Timeout).
- Alle eksterne kald med Timeouts (DB, HTTP, Fileshares). Vælg Timeouts, så de passer ind i dit Shutdown-Timeout.
- Stop-Timeout som konfiguration (f.eks. i INI/Registry), så driften kan reagere uden at skulle re-kompilere.
- Trinmodel: Først graceful (igangværende jobs færdiggøres), derefter eventuelt „soft abort“ (ingen nye skridt), til sidst hard exit som sidste udvej.
- Gode Stop-Logs med faser og tidsmåling.
Konklusion: TEvent + Stop-Timeout er ikke luksus, men styrbarhed
Et hængende Stop er sjældent en enkelt fejl – som regel er det et arkitekturhul: arbejde kører i threads eller i blokerende kald, der ikke deler et fælles Stop-signal. Med et klart Stop-Event (TEvent, Manual Reset), stop-kompatible Waits i stedet for Sleep, konsekvente Timeouts for eksterne afhængigheder og et defineret Shutdown-Timeout får du en Service, der er forudsigelig i daglig drift.
Koden betaler sig især, hvis din Service kører i produktionsmiljøer med vedligeholdelsesvinduer, automatiserede deployments eller kritiske sideeffekter. Så er „Graceful Shutdown“ ikke kosmetik, men en byggesten for stabil drift og færre eskalationer ved næste reboot.
Hvis du vil sætte jeres Stop-sti ordentligt op eller få en eksisterende Delphi-Service gennemgået for robust Shutdown-logik og driftsikkerhed, er et teknisk sparrings-call ofte den hurtigste vej til klare tiltag: Kontakt aufnehmen.
For dette emne er også Delphi Windows Service og Tevent Delphi vigtige. Artiklen placerer disse aspekter forståeligt og viser, hvad der betyder noget i dagligdagen.
Næste trin
Når emnet bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.
Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.
- Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
- REST, dataadgang, portaler og udrulning bliver ikke udskudt som efterfølgende opgaver.
- De ser tidligt, hvilken vej der er økonomisk og driftsmæssigt bæredygtig.