Från magasinets tema till projektpraxis
Passande tjänste- och tekniksidor för inlägget
En Windows Service in Delphi fungerar i vardagen ofta ospektakulärt: körs i bakgrunden, bearbetar jobb, skriver loggar, kommunicerar med databaser eller REST-API:er. Tills någon klickar på „Avsluta tjänst“ – eller en patch-reboot är nödvändig – och tjänsten inte stoppas rent. Då visar tjänstekonsolen i flera minuter „Wird beendet…“, tjänsten fastnar i Stop Pending-status, och i värsta fall avslutas processen på ett hårt sätt. Just här är det värt att behandla Windows Service in Delphi Graceful Shutdown som ett medvetet arkitekturtema: med en tydlig stängningssignal, definierade timeouter och trådar som faktiskt reagerar.
I det här inlägget handlar det inte om ramverksinterna detaljer, utan om ett praktiskt mönster: TEvent som stoppsignal (ett kernelnära synkroniseringsobjekt från System.SyncObjs), kombinerat med en Stop-Timeout-strategi som tar hänsyn både till Windows Service Control Manager (SCM, alltså Windows-komponenten som startar/stoppas tjänster) och dina egna worker-trådar. Dessutom typiska kantfall, debuggningsmetoder och frågan när den extra logiken verkligen lönar sig.
Windows Service in Delphi Graceful Shutdown i praktiken
Den vanligaste orsaken är enkel: tjänsten har minst en tråd som sitter i en blockerande operation och saknar en avbrottsväg. Klassiker:
- Polling-loopar med Sleep: „while not Terminated do Sleep(1000)“. Vid stopp kommer signalen fram, men tråden reagerar först efter upp till 1 sekund (eller 30 sekunder…).
- Blockerande I/O: databas-anrop, HTTP-förfrågningar, Named Pipes, filsystemsväntningar – allt som „bara väntar“ utan att ta hänsyn till ett stoppsignal.
- Queue-Consumer utan väckning: en worker väntar på en kö, men vid stopp väcks den inte för att avsluta.
- Låsordning/Deadlocks: Vid stopp görs „Cleanup“ medan andra trådar fortfarande håller lås. Det inträffar ofta bara i stoppvägen eftersom ordningen där skiljer sig från normal drift.
Windows SCM förväntar sig att en tjänst reagerar snabbt på ett stop-kommand och kontinuerligt rapporterar sin status (via SetServiceStatus; Delphi kapslar in detta i service-komponenten). Om du tar emot ett stopphändelse, men inte stänger ner dina trådar ordentligt, förblir processen levande – och Windows beslutar så småningom att det „tar för lång tid“. Resultatet blir antingen en hård avbrytning eller en tjänst som fastnar i en oklar mellanvärld.
Grundprincip: Ett stoppsignal som varje worker förstår
En Graceful Shutdown fungerar bara om du har en signal som:
- som kan observeras av alla relevanta trådar,
- också från blockerande väntetillstånd har verkan,
- i stoppvägen deterministisk är (ingen „kanske kommer den ut någon gång“-förhoppning),
- en tydlig Timeout-Strategie har.
I Delphi är TEvent ett mycket användbart verktyg för detta: ett event-objekt som internt realiseras via Windows-handles (jämförbart med CreateEvent/SetEvent). Du kan använda det som ett „Stop requested“-signal. Varje worker väntar då inte blint, utan väntar „på arbete eller på Stop“.
TEvent richtig wählen: ManualReset vs. AutoReset
Vid stoppsignaler vill du vanligen ha Manual Reset (manuellt återställbart): När det väl satts förblir eventet „signaled“ tills du återställer det. Det säkerställer att varje tråd som senare går in i ett vänteläge ändå känner igen stoppsignalen. Auto Reset vore här riskabelt, eftersom det automatiskt återställer signalen efter en väntande tråd och andra trådar kan missa stoppsignalen.
Delphi-Service-Lebenszyklus: Var Stop verkligen når fram
En Delphi-Windows- und Linux-Services bygger typiskt på TService (VCL/RTL). SCM skickar kommandon (Start, Stop, Pause, Continue). Delphi anropar sedan motsvarande events/metoder (beroende på template t.ex. OnStart, OnStop, OnExecute).
Viktigt för arkitekturen:
- OnStop är inte platsen för lång väntan utan statusuppdateringar. Det är platsen där du initierar avstängningen och sedan väntar kontrollerat – med timeout.
- OnExecute är ofta en loop. Om du arbetar där „oändligt“ måste loopen reagera på ett Stop-signal.
- Worker-Threads (TThread eller trådpooler) måste reagera på samma Stop-signal, annars är tjänsten logiskt stoppad men fysiskt inte klar.
Rent mönster: Stop-Event + Join av Worker + hård fallback
Det praktiska mönstret består av fyra steg:
- Begär Stop: Sätt Stop-Event, ta inte emot fler jobb.
- Utlös wakeups: Om workers väntar på köer eller i Sleep måste de kunna „vakna“ (t.ex. via event-/kö-signal).
- Avsluta ordnat: Workers avslutar sina loopar, stänger resurser (DB-anslutningar, filer, handles) och rapporterar „färdiga“.
- Timeout och fallback: Om inte allt avslutas i tid måste du fatta ett beslut: vänta vidare (med statusuppdatering) eller kontrollerat avbryta/tvångsavsluta (beroende på risk).
Kärnan är: ingen tråd får enbart vänta på tid (Sleep) eller enbart blockera på I/O utan att parallellt beakta ett Stop-signal. Istället arbetar du med väntfunktioner som kan beakta flera signaler (t.ex. „Stop-Event eller Work-Event“), eller kapslar I/O i timeouter plus stop-kontroller.
Stop-Timeout rätt tänkt: SCM-Timeout vs. egen shutdown-timeout
Här uppstår de flesta missförstånd i projekt. Det finns två olika timeout-nivåer:
- SCM-förväntning: Windows förväntar sig att du i status SERVICE_STOP_PENDING regelbundet rapporterar framsteg. Annars upplevs det som att du har fastnat. Delphi hanterar detta delvis, men så snart du själv blockerar längre behöver du en strategi för hur du möjliggör fortsatta statusuppdateringar (eller hur du håller din stoppfas kort).
- Din egen avstängningstimeout: Du definierar t.ex. „Vi ger oss 20 sekunder för att avsluta pågående jobb ordentligt, sedan avbryter vi.“ Det är ett arkitekturval: datakonsistens vs. tvångsreboot vs. driftkrav.
I praktiken innebär det: Din service bör snabbt komma i ett tillstånd där den inte startar nya arbetsenheter längre, och sedan bara väntar på att pågående arbete ska avslutas – men inte på obestämd tid. Och denna väntfas bör köras i små intervaller så att du kan reagera och vid behov logga.
Hur länge får stoppet ta?
Det finns ingen magisk siffra som alltid passar. För många affärstjänster är ett målintervall på 5–30 sekunder realistiskt: tillräckligt med tid för „in-flight“-data, men kort nog för patchfönster. Om du regelbundet behöver längre tid är det ofta en indikation på att du bearbetar för stora enheter i ett svep eller att externa beroenden (DB/HTTP) kör utan timeout.
Implementering med TEvent: En uppbyggnad som förblir stabil i drift
En beprövad uppbyggnad i Delphi-tjänsten ser ut så här (utan att gå in på ramverksdetaljer):
- En Stop-event (TEvent, Manual Reset) som sätts vid stopp.
- En eller flera Worker-Threads som i sin huvudloop regelbundet kontrollerar för stopp.
- Valfritt ett Work-Event eller en kö som signalerar arbete. Worker väntar då på „Work eller Stop“.
- En shutdown-fas som ‚joinar‘ Worker (dvs väntar tills de är avslutade), men med timeout.
Det avgörande är inte om du använder TThread, omnithreadlibrary eller en egen pool, utan att dina Worker inte kör „blint“. En Worker-loop bör strukturellt se ut så här: Vänta på händelse(r) → arbete i små chunkar → kontrollera stopp mellan chunkarna → frigör resurser ordentligt.
Fallgropar: Terminate räcker inte ensam
Många Delphi-trådar avbryts med Terminate. Det är dock bara en flagga. Om tråden för tillfället sitter i ett blockerat API händer i princip ingenting. Därför är ett eget Stop-event så användbart: du kan integrera det i väntan och trigga avsedda wakeups.
Fallgropar: FreeOnTerminate i servicekontext
I services ser man ofta FreeOnTerminate := True. Det kan fungera, men gör shutdown svårare att kontrollera, eftersom du då ofta inte har en ren referens för att vänta på trådslut och logga fel. För kontrollerad stopplogik är det i regel stabilare att äga trådarna explicit och vid shutdown deterministiskt vänta på dem och frigöra dem.
Blockerande operationer: Så gör du dem stoppbara
Det svåra är inte själva eventet, utan de ställen där din tjänst blockerar. Tre typiska klasser:
1) Sleep/Polling ersetzen: Wait mit Stop-Event
Om du arbetar periodiskt („kontrollera var 10:e sekund“), använd inte Sleep(10000), utan vänta på ett event med timeout. Då kan ditt Stop-Event avbryta väntan omedelbart. Det minskar Stop-Latenz och förhindrar känslan „tjänsten reagerar inte“.
2) Queue-Consumer: Work-Event + Stop-Event kombinieren
Om du har en Producer/Consumer-arkitektur (t.ex. jobb läggs i en kö) behöver du en signal som väcker konsumenterna. Ofta är det ett ytterligare TEvent (Work available). Konsumenten väntar då på två Handles: „Work“ eller „Stop“. Vid Stop sätter du Stop-Event och eventuellt även Work-Event, så att alla konsumenter garanterat kommer ur Wait.
3) Externe Calls (DB/HTTP): Timeouts und Abbruchpfade
Vid databasåtkomst eller HTTP-anrop avgörs om din tjänst stoppas rent. För drift gäller: Inget Call utan Timeout. En timeout är ingen lyx, utan en förutsättning för styrbarhet. Dessutom bör du mellan Retries/Backoff-faser alltid kontrollera Stop. Annars får du klassikern: „Service stoppt nicht, weil er gerade 10 Retries mit Sleep macht“.
I vissa bibliotek kan du trigga avbrott explicit (t.ex. Query-Abbruch). Om det inte går måste du åtminstone konfigurera så att Timeouts är tillräckligt korta för att inte spränga Shutdown-Timeout.
Stop Pending korrekt: Status, Logging und Erwartungsmanagement
När en tjänst stoppas är det ur driftsperspektiv viktigt att förstå var den hänger. För det behöver du två saker:
- Log-Marker i stop-påverkade sökvägen: „Stop angefordert“, „keine neuen Jobs“, „warte auf Worker“, „Worker X beendet“, „Shutdown fertig“.
- Mätbara tider: Hur lång tid tar stoppet? Vilken fas tar tid? Ofta räcker ett monotont tidsmått som GetTickCount64 eller TStopwatch (monoton = inte förvrängt av systemtidsändringar).
Om du i stoppsekvensen bara skriver en enda loggpost „Stopping…“ förblir felsökningen i fältet ett gissningsspel. I tjänstmiljö är loggar ofta det enda du får utan interaktion.
Vilka loggar är verkligen hjälpsamma i tjänster?
- Service-PID, starttid, version/build (utan överdriven overhead).
- Antal aktiva worker, antal pågående jobb.
- Aktiva externa beroenden: „DB-Call pågår“, „HTTP-Request pågår“, „Datei-Flush pågår“ (endast aggregerat, inte varje detalj).
- Stop-timeout uppnått: vilka worker är fortfarande aktiva?
Felsökning i fält: gör det reproducerbart istället för att gissa
Stopproblem uppstår ofta endast i produktion: annan belastning, andra latenser, andra rättigheter, andra patch-fönster. Några praxiserprobade åtgärder:
Testa tjänsten under kontrollerade förhållanden
- Stopp under aktiv bearbetning (inte i viloläge).
- Stopp under extern störning: DB kort otillgänglig, HTTP-endpunkt långsam, fileshare borta.
- Stopp omedelbart efter start (Race-Conditions: worker fortfarande under uppbyggnad).
Event Viewer och Service Control Manager-signaler
Windows skriver tjänstehändelser, men de är ofta grova. Bättre är om din service själv skriver till en loggfil eller Windows Event Log. Viktigt är: loggning bör fortfarande fungera i stoppsekvensen. Om du friger loggern för tidigt i shutdown eller om Flush blockeras, förlorar du precis de avgörande spåren.
Gör hängande trådar synliga
Om du upprepade gånger ser „Stop Timeout“ är det värt att titta på trådtillstånden (t.ex. med debugger/Procdump i testmiljö). Ofta hittar du då en tråd i ett väntillstånd på ett handle som aldrig signaleras, eller i ett nätverksanrop utan timeout. Fixen är sällan „ännu mer Sleep“, utan en ordentlig avbrytningsväg.
När lönar sig ansträngningen verkligen?
En minimalistisk tjänst som bara har en timer och inga externa beroenden kan ibland „stanna enkelt“. Men så fort någon av följande gäller är en ordnad Graceful Shutdown nästan alltid värd insatsen:
- Tjänsten bearbetar jobb med sidoeffekter (skriva filer, DB-transaktioner, API-anrop).
- Det finns flera trådar eller en pool.
- Tjänsten är beroende av nätverksresurser (DB, REST, Message Broker, Fileshares).
- Drift kräver planerbara underhållsfönster (omstarter, uppdateringar, failover).
Värdet är inte „Eleganz“, utan driftssäkerhet: färre hårda processavbrott, färre inkonsistenta mellantillstånd, färre manuella ingrepp.
Praktiska fallgropar: vad som ofta går fel vid shutdown
1) Stopp har satts, men nya jobb fortsätter komma in
Om du tar emot inkommande arbete (t.ex. via socket, fil-trigger, timer) måste du i stoppsekvensen först stoppa accepten av nytt arbete: stäng lyssnare, inaktivera timers, stoppa schemaläggaren. Annars springer du efter slutet eftersom nya jobb fortfarande startar.
2) Cleanup blockerar (Flush, Close, Finalize)
„Bara snabbt flusha allt“ kan vara farligt i tjänstekontext om målet (nätverksdisk, Remote-Log, DB) just hänger. Därför: cleanup ja, men med begränsad tid. I nödfall måste du besluta vilka data du förlorar i minnet istället för att blockera hela stoppen.
3) Lås och ordningsföljd
När Stop ofta når du samma datastrukturer som Worker (Queues, Caches, States). Om Stop-tråden håller lås och sedan väntar på att Worker ska avslutas, samtidigt som Worker behöver samma lås, får du ett Stop-Deadlock. Motåtgärder: håll låshållningstider korta, vänta inte „under lås“ i Stop-sökvägen, definiera en tydlig ordning.
4) Konkurrens vid dubbla Stop
I praktiken kan Stop triggas flera gånger (t.ex. Stop + Shutdown, eller Stop kommer igen). Din Stop-sökväg bör vara idempotent: att sätta Stop-eventet är okej, men dubbel Join/Free-logik måste skyddas ordentligt (t.ex. via en atomär flagga).
Operativt perspektiv: vad administratörer och IT-ansvariga förväntar sig av tjänsten
För drift och administration spelar det slutligen ingen roll hur „fint“ koden är, utan om tjänsten:
- vid Stop avslutas pålitligt (förutsägbart, utan att fastna),
- vid Stop skapar inga inkonsekventa data (t.ex. halva filer, öppna transaktioner),
- vid fel levererar användbara loggar,
- vid underhållsfönster och deployment är förutsägbar.
Det är också anledningen till att ämnet Stop-Timeout inte bara är „utvecklarsaker“: det påverkar patchcykler, återställningstider och frågan om automatiserade deployments överhuvudtaget är möjliga.
Konkreta riktlinjer för en robust Shutdown-design
Om du vill standardisera ämnet pragmatiskt har dessa riktlinjer visat sig effektiva:
- En global Stop-Event, Manual Reset, skapas tidigt i tjänstens livscykel, frigörs sent.
- Inget Sleep i Worker-loopar utan Stop-kompatibelt alternativ (Wait med timeout).
- Alla externa anrop med timeouter (DB, HTTP, filresurser). Välj timeouter så att de ryms inom ditt Shutdown-Timeout.
- Stop-Timeout som konfiguration (t.ex. i INI/Registry), så att drift kan reagera utan att behöva kompilera om.
- Stegsmodell: Först graceful (pågående jobb får slutföras), sedan valfri „soft abort“ (inga nya steg), och slutligen ett hårt exit som sista utväg.
- Bra Stop-Logs med faser och tidmätning.
Slutsats: TEvent + Stop-Timeout är ingen lyx, utan styrbarhet
En hängande Stop är sällan ett enstaka fel – oftast är det ett arkitekturproblem: arbete körs i trådar eller blockerande anrop som inte känner till ett gemensamt Stop-signal. Med ett tydligt Stop-Event (TEvent, Manual Reset), Stop-kompatibla Waits istället för Sleep, konsekventa timeouter för externa beroenden och ett definierat Shutdown-Timeout får du en tjänst som är förutsägbar i vardagen.
Koden lönar sig särskilt om din tjänst körs i produktionsmiljöer med underhållsfönster, automatiserade deployments eller kritiska sidoeffekter. Då är „Graceful Shutdown“ inte kosmetik, utan en byggsten för stabil drift och färre eskalationer vid nästa omstart.
Om du vill sätta upp er Stop-sökväg ordentligt eller granska en befintlig Delphi-tjänst för robust shutdown-logik och driftssäkerhet, är ett tekniskt sparringssamtal ofta det snabbaste sättet till konkreta åtgärder: Ta kontakt.
För detta ämne är också Delphi Windows Service och Tevent Delphi viktiga. Inlägget placerar dessa aspekter tydligt och visar vad som är viktigt i vardagen.
nästa steg
När ett ämne blir ett verkligt projekt bör arkitektur, befintligt bestånd och drift tidigt ses över gemensamt.
Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.
- Nuläge, målbild och tekniska risker bedöms tillsammans.
- REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
- Ni ser tidigt vilken väg som är ekonomiskt och driftmässigt hållbar.