Fra magasinetema til prosjektpraksis
Egnede tjeneste- og tekniske sider for innlegget
En Windows Service in Delphi fremstår i hverdagen ofte som uspektakulær: kjører i bakgrunnen, behandler jobber, skriver logger, snakker med databaser eller REST-APIer. Inntil noen klikker «Stopp tjeneste» — eller et patch-reboot er nødvendig — og tjenesten ikke stopper ryddig. Da viser tjenestekonsollen i flere minutter «Stopper…», tjenesten henger i Stop Pending-status, og i verste fall blir prosessen tvangsavsluttet. Nettopp her lønner det seg å behandle Windows Service in Delphi Graceful Shutdown som et bevisst arkitekturtema: med et klart shutdown-signal, definerte timeouter og tråder som faktisk reagerer.
I dette innlegget handler det ikke om rammeverksinterne detaljer, men om et praktisk mønster: TEvent som stoppsignal (et kernel-nært synkroniseringsobjekt fra System.SyncObjs), kombinert med en Stop-Timeout-strategi som tar hensyn til både Windows Service Control Manager (SCM, altså die Windows-Komponente, die Dienste startet/stoppt) og dine egne Worker-tråder. I tillegg typiske randtilfeller, feilsøkingsmetoder og spørsmålet om når den ekstra logikken virkelig lønner seg.
Windows Service in Delphi Graceful Shutdown i praksis
Den vanligste årsaken er enkel: tjenesten har minst én tråd som sitter i en blokkering og ikke har en avbruddsvei. Klassikere:
- Polling-Schleifen mit Sleep: „while not Terminated do Sleep(1000)“. Ved stopp kommer signalet, men tråden reagerer først etter opptil 1 sekund (eller 30 sekunder…).
- Blockierende I/O: databasekall, HTTP-requests, Named Pipes, Filesystem-Waits – alt som «bare venter» uten å lytte etter et stoppsignal.
- Queue-Consumer ohne Wakeup: En worker venter på en kø, men ved stopp blir den ikke vekket for å avslutte.
- Lock-Reihenfolge/Deadlocks: Ved stopp utføres «Cleanup» mens andre tråder fortsatt holder låser. Dette opptrer ofte bare i stoppstien, fordi rekkefølgen der er annerledes enn i normal drift.
Windows SCM forventer at en tjeneste reagerer raskt på et stoppsignal og løpende rapporterer sin status (via SetServiceStatus; Delphi kapsler dette i service-komponenten). Hvis du tar imot en stopphendelse, men ikke stopper trådene dine ryddig, forblir prosessen levende — og Windows avgjør etter en stund at det «tar for lang tid». Resultatet er enten et hardt avbrudd eller en tjeneste som henger igjen i et uklart mellomstadium.
Grunnprinsipp: Et stoppsignal som hver Worker forstår
En Graceful Shutdown fungerer kun hvis du har et signal som:
- kan observeres av alle relevante tråder,
- også fra blokkende ventetilstander kan få virkning,
- i stoppløpet deterministisk er (ingen «kanskje kommer den ut en gang»-håp),
- en klar timeout-strategi har.
I Delphi er TEvent et svært anvendelig verktøy for dette: et event-objekt som internt realiseres via Windows-handles (sammenlignbart med CreateEvent/SetEvent). Du kan bruke det som et «Stop requested»-signal. Hver Worker venter da ikke blindt, men venter «på arbeid eller på stopp».
TEvent riktig valg: ManualReset vs. AutoReset
Ved stoppsignaler vil du vanligvis ha Manual Reset (manuelt tilbakestillbar): Når det settes, forblir eventet «signaled» til du tilbakestiller det. Det sikrer at enhver tråd som senere går inn i en ventefase likevel gjenkjenner stoppsignalet. Auto Reset er risikabelt her, fordi det tilbakestiller signalet automatisk etter en ventende tråd og andre tråder kan gå glipp av stoppsignalet.
Delphi-Service-Lebenszyklus: Hvor stopp faktisk når fram
En Delphi-Windows- und Linux-Services bygger typisk på TService (VCL/RTL). SCM sender kommandoer (Start, Stop, Pause, Continue). Delphi kaller deretter tilsvarende events/metoder (avhengig av template f.eks. OnStart, OnStop, OnExecute).
Viktig for arkitekturen:
- OnStop er ikke stedet for langvarig venting uten statusoppdateringer. Det er stedet hvor du initierer shutdown og deretter venter kontrollert – med timeout.
- OnExecute er ofte en løkke. Hvis du jobber der «endeløst», må løkken reagere på et stoppsignal.
- Worker-Threads (TThread eller thread-pools) må reagere på samme stoppsignal, ellers er servicen logisk stoppet, men fysisk ikke ferdig.
Rent mønster: Stop-Event + Join av Worker + tvungen fallback
Det praktiske mønsteret består av fire steg:
- Be om stopp: Sett Stop-Event, ta ikke imot nye jobber.
- Fremkall wakeups: Hvis Worker venter på køer eller i Sleep, må de kunne «vekkes» (f.eks. via Event-/Queue-signal).
- Avslutt ordnet: Worker avslutter løkkene sine, lukker ressurser (DB-tilkoblinger, filer, handles) og rapporterer «ferdig».
- Timeout og fallback: Hvis ikke alt avslutter i tide, må du ta en beslutning: vente videre (med statusoppdatering) eller kontrollert abort/tvangsavslutning (avhengig av risiko).
Kjernen er: ingen tråd må utelukkende vente på tid (Sleep) eller utelukkende blokkere på I/O uten samtidig å ta hensyn til et stoppsignal. I stedet bruker du ventefunksjoner som vurderer flere signaler (f.eks. «Stop-Event eller Work-Event»), eller du kapsler I/O i timeouter pluss stoppsjekker.
Tenke riktig rundt Stop-Timeout: SCM-Timeout vs. egen Shutdown-Timeout
Her oppstår de fleste misforståelser i prosjekter. Det finnes to forskjellige timeout-nivåer:
- SCM-forventning: Windows forventer at du i status SERVICE_STOP_PENDING regelmessig rapporterer fremdrift. Ellers vil det fremstå som om du henger. Delphi tar delvis hånd om dette, men så snart du selv blokkerer over lengre tid, trenger du en strategi for hvordan du fortsetter å gi statusoppdateringer (eller hvordan du holder stopp-fasen kort).
- Din egen shutdown-timeout: Du definerer f.eks. «Vi gir oss 20 sekunder for å fullføre løpende jobber på en ryddig måte, så avbryter vi.» Det er et arkitekturvalg: datakonsistens vs. tvungen omstart vs. driftspolicy.
I praksis betyr det: Tjenesten din bør raskt komme i en tilstand hvor den ikke starter nye arbeidsenheter lenger, og deretter kun venter på at pågående arbeid skal avsluttes – men ikke på ubestemt tid. Og denne ventefasen bør kjøres i korte intervaller, slik at du kan reagere og eventuelt logge.
Hvor lenge kan stoppet vare?
Det finnes ingen magisk tall som alltid passer. For mange forretningstjenester er et målområde på 5–30 sekunder realistisk: nok tid for «in-flight»-data, men kort nok for patch-vinduer. Hvis du regelmessig trenger lengre tid, er det ofte et tegn på at du behandler for store enheter av gangen eller at eksterne avhengigheter (DB/HTTP) kjører uten timeout.
Implementering med TEvent: Oppbygning som forblir stabil i drift
En velprøvd oppbygning i Delphi-tjenesten ser slik ut (uten å gå i dybden på rammeverksdetaljer):
- Et Stop-Event (TEvent, Manual Reset) som settes ved stopp.
- En eller flere Worker-Threads, som i sin hovedsløyfe regelmessig sjekker etter Stop.
- Valgfritt et Work-Event eller en kø som signaliserer arbeid. Worker venter da på «Work eller Stop».
- En Shutdown-fase som «joined» workerne (altså venter til de er ferdige), men med timeout.
Det avgjørende er ikke om du bruker TThread, omnithreadlibrary eller en egen pool, men at workerne dine ikke kjører «blindt». En worker-loop bør strukturelt se slik ut: vent på hendelse(r) → arbeid i små chunks → mellom chunkene sjekk Stop → frigjør ressurser ryddig.
Fallgruve: Terminate alene er ikke nok
Mange Delphi-tråder blir med Terminate «avbrutt». Dette er imidlertid bare et flagg. Hvis tråden befinner seg i en blokkerende API, skjer det i praksis ingenting. Derfor er et eget Stop-Event så nyttig: Du kan integrere det i ventekall og utløse målrettede wakeups.
Fallgruve: FreeOnTerminate i servicekontekst
I tjenester ser man ofte FreeOnTerminate := True. Det kan fungere, men gjør shutdown vanskeligere å kontrollere, fordi du ofte ikke lenger har en ryddig referanse å vente på for trådslutt og for å loggføre feilsituasjoner. For kontrollert stopplogikk er det vanligvis mer stabilt å eie trådene eksplisitt og under shutdown deterministisk vente på dem og frigjøre dem.
Blokkerende operasjoner: Slik gjør du dem stoppbare
Den vanskelige delen er ikke selve hendelsen, men stedene hvor tjenesten din blokkerer. Tre typiske klasser:
1) Erstatte Sleep/Polling: Vent med Stop-Event
Hvis du arbeider periodisk («sjekk hvert 10. sekund»), bruk ikke Sleep(10000), men vent på et event med timeout. Da kan Stop-Eventet ditt avslutte ventingen umiddelbart. Det reduserer stopp-latens og forhindrer inntrykket «tjenesten reagerer ikke».
2) Queue-consumer: Kombiner Work-Event og Stop-Event
Hvis du har en Producer/Consumer-arkitektur (f.eks. at jobber legges i en kø), trenger du et signal som vekker consumerne. Oft er dette et ekstra TEvent (Work available). Consumeren venter da på to handles: «Work» eller «Stop». Ved stop setter du Stop-Eventet og eventuelt også Work-Eventet, slik at alle consumerne garantert kommer ut av ventingen.
3) Eksterne kall (DB/HTTP): Timeouts og avbruddsveier
Ved databaseforespørsler eller HTTP-kall avgjøres om tjenesten stopper rent. I drift gjelder: Ingen kall uten timeout. En timeout er ikke en luksus, men en forutsetning for styrbarhet. I tillegg bør du alltid sjekke Stop mellom retry-/backoff‑faser. Ellers får du klassikeren: «Tjenesten stopper ikke fordi den akkurat kjører 10 retries med Sleep».
I noen biblioteker kan du trigge avbrudd eksplisitt (f.eks. query‑avbrudd). Hvis det ikke er mulig må du i det minste konfigurere timeouts korte nok til å ikke overskride shutdown‑timeouten.
Stop Pending korrekt: Status, logging og forventningsstyring
Når en tjeneste stopper, er det fra et driftsperspektiv viktig å forstå, hvor den henger. Til det trenger du to ting:
- Log‑markører i stopp‑veien: «Stop anmodet», «ingen nye jobber», «venter på worker», «Worker X avsluttet», «shutdown ferdig».
- Målbare tider: Hvor lenge varer stoppet? Hvilken fase bruker opp tid? Ofte holder et monotont tidsmål som GetTickCount64 eller TStopwatch (monotont = ikke forvrengt av systemtidsendringer).
Hvis du i stoppstien kun skriver én enkelt loggoppføring „Stopping…“, forblir feilsøking i felten et gjettespill. I tjenestedrift er logger ofte det eneste du får uten interaksjon.
Hvilke logger er virkelig nyttige i tjenester?
- Service-PID, starttid, versjon/Build (uten overdreven overhead).
- Antall aktive workere, antall in-flight jobber.
- Aktive eksterne avhengigheter: „DB-Call läuft“, „HTTP-Request läuft“, „Datei-Flush läuft“ (kun aggregert, ikke hvert detalj).
- Stopp-timeout nådd: hvilke workere er fortsatt åpne?
Feilsøking i felten: gjøre det reproduserbart i stedet for å gjette
Stopp-problemene oppstår ofte bare i produksjon: annen belastning, andre latenser, andre rettigheter, andre patch-vinduer. Noen praktisk utprøvde grep:
Teste tjenesten under kontroll
- Stopp under aktiv behandling (ikke i idle).
- Stopp under ekstern feil: DB kort utilgjengelig, HTTP-endepunkt tregt, Fileshare borte.
- Stopp umiddelbart etter oppstart (race-conditions: workere fortsatt under oppbygging).
Event Viewer og Service Control Manager-signaler
Windows skriver tjenestehendelser, men de er ofte grove. Bedre er det om tjenesten din selv skriver til en loggfil eller Windows Event Log. Viktig: logging må fortsatt fungere i stoppstien. Hvis du frigir loggerne for tidlig i shutdown eller flush blokkeres, mister du nettopp de avgjørende sporene.
Gjør hengende tråder synlige
Hvis du gjentatte ganger ser „Stop Timeout“, er det verdt å se på trådenes tilstand (f.eks. med debugger/Procdump i testmiljø). Ofte finner du en tråd i en wait-tilstand på et handle som aldri blir signalisert, eller i et nettverksanrop uten timeout. Løsningen er sjelden „mer sleep“, men en ren avbruddsvei.
Når er innsatsen virkelig verdt det?
En minimalistisk tjeneste som kun har en timer og ingen eksterne avhengigheter kan noen ganger „bare stoppe“. Men så snart ett av følgende gjelder, lønner et skikkelig Graceful Shutdown seg nesten alltid:
- Tjenesten behandler jobber med sideeffekter (skrive filer, DB-transaksjoner, API-kall).
- Det finnes flere tråder eller en pool.
- Tjenesten er avhengig av nettverksressurser (DB, REST, Message Broker, Fileshares).
- Drift krever planlagte vedlikeholdsvinduer (reboots, oppdateringer, failover).
Gevinsten er ikke „eleganse“, men driftssikkerhet: færre harde prosessavbrudd, færre inkonsistente mellomtilstander, færre manuelle inngrep.
Praktiske fallgruver: Hva som ofte går galt i Shutdown
1) Stopp er satt, men nye jobber kommer fortsatt inn
Hvis du tar imot innkommende arbeid (f.eks. via socket, fil-trigger, timer), må du i stoppstien først stoppe mottak av nytt arbeid: lukke lyttere, deaktivere timere, stoppe scheduler. Ellers løper du etter slutten fordi nye jobber fortsatt starter.
2) Cleanup blokkerer (Flush, Close, Finalize)
„Bare raskt flush alt“ kan være farlig i tjenestekontekst hvis målet (nettverksdisk, remote-logg, DB) henger. Derfor: Cleanup ja, men med begrenset tid. Hvis nødvendig må du avgjøre hvilke data du mister i minnet, heller enn å blokkere hele stoppet.
3) Låser og rekkefølge
Beim Stop greifst du häufig auf dieselben Datenstrukturen zu wie die Worker (Queues, Caches, States). Wenn der Stop-Thread Locks hält und dann auf Worker-Ende wartet, während Worker denselben Lock brauchen, hast du einen Stop-Deadlock. Gegenmittel: Lock-Hold-Zeiten klein halten, im Stop-Pfad nicht „unter Lock warten“, klare Reihenfolge definieren.
4) Nebenläufigkeit beim doppelten Stop
In der Praxis kann Stop mehrfach getriggert werden (z. B. Stop + Shutdown, oder Stop kommt erneut). Dein Stop-Pfad sollte idempotent sein: Stop-Event setzen ist okay, aber doppelte Join/Free-Logik muss sauber geschützt werden (z. B. über ein Atomik-Flag).
Operativer Blick: Was Admins und IT-Leads vom Service erwarten
Für Betrieb und Administration zählt am Ende nicht, wie „schön“ der Code ist, sondern ob der Dienst:
- bei Stop verlässlich endet (planbar, ohne Hänger),
- bei Stop keine inkonsistenten Daten produziert (z. B. halbe Dateien, offene Transaktionen),
- im Fehlerfall brauchbare Logs liefert,
- bei Wartungsfenstern und Deployments berechenbar ist.
Das ist auch der Grund, warum das Thema Stop-Timeout nicht nur „Entwicklerkram“ ist: Es beeinflusst Patchzyklen, Recovery-Zeiten und die Frage, ob automatisierte Deployments überhaupt möglich sind.
Konkrete Leitplanken für ein robustes Shutdown-Design
Wenn du das Thema pragmatisch standardisieren willst, haben sich diese Leitplanken bewährt:
- Ein globales Stop-Event, Manual Reset, früh im Service-Lebenszyklus erstellt, spät freigegeben.
- Kein Sleep in Worker-Loops ohne Stop-fähige Alternative (Wait mit Timeout).
- Alle externen Calls mit Timeouts (DB, HTTP, Fileshares). Timeouts so wählen, dass sie in deinen Shutdown-Timeout passen.
- Stop-Timeout als Konfiguration (z. B. in INI/Registry), damit Betrieb reagieren kann, ohne neu zu kompilieren.
- Stufenmodell: Erst graceful (laufende Jobs zu Ende), dann optional „soft abort“ (keine neuen Schritte), dann harter Exit als letzter Ausweg.
- Gute Stop-Logs mit Phasen und Zeitmessung.
Fazit: TEvent + Stop-Timeout ist kein Luxus, sondern Steuerbarkeit
Ein hängender Stop ist selten ein Einzelfehler – meist ist es ein Architekturloch: Arbeit läuft in Threads oder blockierenden Calls, die kein gemeinsames Stop-Signal kennen. Mit einem klaren Stop-Event (TEvent, Manual Reset), stop-fähigen Waits statt Sleep, konsequenten Timeouts für externe Abhängigkeiten und einem definierten Shutdown-Timeout bekommst du einen Service, der im Alltag berechenbar ist.
Der Code lohnt sich besonders, wenn dein Service in Produktionsumgebungen mit Wartungsfenstern, automatisierten Deployments oder kritischen Seiteneffekten arbeitet. Dann ist „Graceful Shutdown“ nicht Kosmetik, sondern ein Baustein für stabilen Betrieb und weniger Eskalationen beim nächsten Reboot.
Wenn du euren Stop-Pfad einmal sauber aufsetzen oder einen bestehenden Delphi-Service auf robuste Shutdown-Logik und Betriebssicherheit überprüfen willst, ist ein technischer Sparrings-Call oft der schnellste Weg zu klaren Maßnahmen: Kontakt aufnehmen.
Für dieses Thema sind auch Delphi Windows Service und Tevent Delphi wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Neste trinn
Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift vurderes samlet allerede tidlig i prosessen.
Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.
- Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
- REST, datatilgang, portaler og utrulling blir ikke utsatt som etterfølgende oppgaver.
- Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.