Du thème du magazine à la pratique des projets
Pages de services et techniques pertinentes pour l'article
Un Windows Service in Delphi semble souvent anodin au quotidien : il s’exécute en arrière-plan, traite des jobs, écrit des logs, interroge des bases de données ou des APIs REST. Jusqu’à ce que quelqu’un clique sur «Arrêter le service» – ou qu’un patch-reboot soit nécessaire – et que le service ne s’arrête pas proprement. Alors la console des services affiche pendant des minutes «Arrêt en cours…», le service reste bloqué en Stop Pending, et dans le pire des cas le processus est tué de force. C’est précisément là qu’il est pertinent d’aborder le Windows Service in Delphi Graceful Shutdown comme un sujet d’architecture : avec un signal d’arrêt clair, des timeouts définis et des threads qui réagissent réellement.
Cet article ne traite pas des internals d’un framework, mais d’un modèle applicable en pratique : TEvent comme signal d’arrêt (un objet de synchronisation proche du kernel issu de System.SyncObjs), combiné à une stratégie de Stop-Timeout qui prend en compte à la fois le Windows Service Control Manager (SCM, donc la composante Windows qui démarre/arrête les services) et vos propres worker-threads. S’y ajoutent les cas limites typiques, des approches de debug et la question de savoir quand cette logique supplémentaire en vaut vraiment la peine.
Windows Service in Delphi Graceful Shutdown en pratique
La cause la plus fréquente est simple : le service comporte au moins un thread qui se trouve dans une opération bloquante et ne connaît pas de chemin d’annulation. Classiques :
- Boucles de sondage avec Sleep : „while not Terminated do Sleep(1000)“. À l’arrêt le signal arrive, mais le thread ne réagit qu’après jusqu’à 1 seconde (ou 30 secondes…).
- I/O bloquantes : appels vers des bases de données, requêtes HTTP, Named Pipes, attentes sur le système de fichiers – tout ce qui « attend simplement », sans tenir compte d’un signal d’arrêt.
- Consommateur de file sans réveil : un worker attend sur une queue, mais à l’arrêt il n’est pas réveillé pour sortir.
- Ordre des locks / deadlocks : lors de l’arrêt une opération de nettoyage est effectuée pendant que d’autres threads détiennent encore des locks. Cela survient souvent uniquement dans le chemin d’arrêt, car l’ordre y est différent de l’exécution normale.
Le Windows SCM s’attend à ce qu’un service réagisse rapidement à une commande d’arrêt et signale en continu son état (via SetServiceStatus ; Delphi encapsule cela dans le composant service). Si vous acceptez bien un événement d’arrêt mais que vos threads ne s’arrêtent pas proprement, le processus reste vivant – et Windows finit par décider que « c’est trop long ». Le résultat est alors soit un arrêt brutal, soit un service coincé dans un état intermédiaire indéfini.
Principe de base : un signal d’arrêt que chaque worker comprend
Un Graceful Shutdown ne fonctionne que si vous disposez d’un signal qui :
- peut être observé par tous les threads pertinents,
- agit même depuis des états d’attente bloquants,
- est déterministe dans le chemin d’arrêt (pas d’espoir du type « peut‑être qu’il en sortira un jour »),
- dispose d’une stratégie de timeout claire.
Dans Delphi, TEvent est un outil très utile pour cela : un objet d’événement implémenté en interne via des handles Windows (comparable à CreateEvent/SetEvent). Vous pouvez l’utiliser comme signal « arrêt demandé ». Chaque worker n’attend alors pas aveuglément, mais attend « du travail ou l’arrêt ».
TEvent richtig wählen: ManualReset vs. AutoReset
Pour les signaux d’arrêt, on souhaite généralement Manual Reset (réinitialisable manuellement) : une fois réglé, l’événement reste « signalé » jusqu’à ce que vous le réinitialisiez. Cela garantit que tout thread arrivant ultérieurement en phase d’attente reconnaît malgré tout le signal d’arrêt. Auto Reset serait risqué ici, car il réinitialise automatiquement le signal après un thread en attente et d’autres threads pourraient manquer le signal d’arrêt.
Delphi-Service-Lebenszyklus: Wo Stop wirklich ankommt
Un Delphi-Windows- und Linux-Services est typiquement basé sur TService (VCL/RTL). Le SCM envoie des commandes (Start, Stop, Pause, Continue). Delphi appelle ensuite les events/méthodes correspondants (selon le template, p. ex. OnStart, OnStop, OnExecute).
Important pour l’architecture :
- OnStop n’est pas l’endroit pour attendre longtemps sans mise à jour de statut. C’est l’endroit où vous initiez l’arrêt puis attendez de manière contrôlée — avec timeout.
- OnExecute est souvent une boucle. Si vous y travaillez « en continu », la boucle doit réagir à un signal d’arrêt.
- Worker-Threads (TThread ou pools de threads) doivent réagir au même signal d’arrêt ; sinon le service est logiquement arrêté mais pas encore terminé physiquement.
Sauberes Muster: Stop-Event + Join der Worker + harter Fallback
Le modèle applicable en pratique se compose de quatre étapes :
- Demander l’arrêt : définir le Stop-Event, n’accepter plus de nouveaux jobs.
- Déclencher les wakeups : si les workers attendent sur des queues ou sont en sleep, ils doivent pouvoir « se réveiller » (p. ex. via un signal Event/Queue).
- Terminer de manière ordonnée : les workers quittent leurs boucles, ferment les ressources (connexions DB, fichiers, handles) et signalent « terminé ».
- Timeout et repli : si tout ne se termine pas à temps, il faut prendre une décision : continuer à attendre (avec mise à jour de statut) ou abandonner de façon contrôlée/forcer l’arrêt (selon le risque).
Le point central est : aucun thread ne doit se contenter d’attendre sur une durée (Sleep) ni se bloquer uniquement sur de l’I/O sans prendre en compte parallèlement un signal d’arrêt. Travaillez plutôt avec des fonctions d’attente qui considèrent plusieurs signaux (p. ex. « Stop-Event ou Work-Event »), ou encapsulez l’I/O dans des timeouts plus vérifications de Stop.
Stop-Timeout richtig denken: SCM-Timeout vs. eigener Shutdown-Timeout
C’est là que surviennent la plupart des malentendus dans les projets. Il existe deux niveaux de timeout différents :
- Attente du SCM : Windows attend que vous signaliez régulièrement l’avancement lorsque vous êtes dans l’état SERVICE_STOP_PENDING. Sinon, cela donne l’impression que vous êtes bloqué. Delphi s’en occupe partiellement, mais dès que vous êtes bloqué plus longtemps, il vous faut une stratégie pour continuer à fournir des mises à jour d’état (ou pour maintenir courte votre phase d’arrêt).
- Votre propre délai d’arrêt : vous définissez par exemple « Nous nous donnons 20 secondes pour terminer proprement les tâches en cours, puis nous abandonnons. » C’est une décision d’architecture : cohérence des données vs. obligation de redémarrage vs. contraintes opérationnelles.
Concrètement : votre service doit rapidement atteindre un état où il ne démarre plus de nouvelles unités de travail, puis se contenter de attendre la fin du travail en cours — mais pas indéfiniment. Et cette phase d’attente doit s’exécuter par petits intervalles, afin que vous puissiez réagir et, le cas échéant, consigner des événements.
Combien de temps l’arrêt peut-il durer ?
Il n’existe pas de chiffre magique qui convienne toujours. Pour de nombreux services métiers, une plage cible de 5–30 secondes est réaliste : suffisamment de temps pour les données « in-flight », mais assez courte pour les fenêtres de patch. Si vous avez régulièrement besoin de plus, c’est souvent un signe que vous traitez des unités trop volumineuses d’un seul bloc ou que des dépendances externes (DB/HTTP) fonctionnent sans timeout.
Implémentation avec TEvent : architecture stable en production
Une architecture éprouvée pour le service Delphi ressemble à ceci (sans entrer dans les détails d’un framework) :
- Un Stop-Event (TEvent, Manual Reset), qui est activé lors de l’arrêt.
- Un ou plusieurs Worker-Threads, qui vérifient régulièrement le Stop dans leur boucle principale.
- Éventuellement un Work-Event ou une queue qui signale du travail. Les workers attendent alors « Work ou Stop ».
- Une phase de shutdown qui réalise le « join » des workers (c.-à-d. attend leur fin), mais avec timeout.
L’important n’est pas que vous utilisiez TThread, omnithreadlibrary ou un pool maison, mais que vos workers ne tournent pas « à l’aveugle ». Une boucle de worker devrait structurellement ressembler à : Attente d’événement(s) → travail en petits morceaux → vérification du Stop entre les morceaux → libération propre des ressources.
Piège : Terminate seul ne suffit pas
Beaucoup de threads Delphi sont « interrompus » avec Terminate. Mais ce n’est qu’un flag. Si le thread est bloqué dans une API bloquante, rien ne se passe. C’est pourquoi un Stop-Event dédié est utile : vous pouvez l’intégrer aux appels d’attente et déclencher des réveils ciblés.
Piège : FreeOnTerminate dans le contexte d’un service
Dans les services, on voit souvent FreeOnTerminate := True. Cela peut fonctionner, mais rend le shutdown plus difficile à contrôler, car vous perdez souvent une référence propre pour attendre la fin du thread et consigner les états d’erreur. Pour une logique d’arrêt contrôlée, il est généralement plus stable de posséder explicitement les threads et de les attendre et libérer de manière déterministe lors du shutdown.
Opérations bloquantes : comment les rendre interrompables
La partie délicate n’est pas l’événement lui‑même, mais les endroits où votre service se bloque. Trois classes typiques :
1) Remplacer Sleep/Polling : utiliser un Wait avec événement d’arrêt
Si vous exécutez des tâches périodiquement (« vérifier toutes les 10 secondes »), n’utilisez pas Sleep(10000), mais attendez un événement avec timeout. Ainsi, votre événement d’arrêt peut interrompre l’attente immédiatement. Cela réduit la latence d’arrêt et évite l’impression « le service ne répond pas ».
2) Consommateur de file : combiner événement de travail + événement d’arrêt
Si vous avez une architecture producteur/consommateur (p. ex. des jobs placés dans une file), vous avez besoin d’un signal qui réveille les consommateurs. Souvent il s’agit d’un TEvent supplémentaire (Work available). Le consommateur attend alors sur deux handles : « Work » ou « Stop ». Lors de l’arrêt, vous signalez l’événement d’arrêt et, si nécessaire, aussi l’événement de travail, afin que tous les consommateurs sortent assurément du Wait.
3) Appels externes (DB/HTTP) : timeouts et chemins d’interruption
Pour les accès base de données ou les appels HTTP, il se joue la question de savoir si votre service s’arrête proprement. En exploitation : Aucun appel sans timeout. Un timeout n’est pas un luxe, mais une condition de pilotabilité. De plus, vous devez vérifier l’arrêt entre les phases de retry/backoff. Sinon vous aurez le classique : « le service ne s’arrête pas parce qu’il effectue 10 retries avec Sleep ».
Avec certaines bibliothèques, vous pouvez déclencher explicitement des annulations (p. ex. annulation de requête). Si ce n’est pas possible, configurez au minimum des timeouts assez courts pour ne pas dépasser le timeout d’arrêt.
Stop Pending korrekt: Status, Logging und Erwartungsmanagement
Quand un service s’arrête, il est important du point de vue exploitation de comprendre où il bloque. Pour cela, vous avez besoin de deux choses :
- Repères de journalisation dans le chemin d’arrêt : « arrêt demandé », « plus de nouveaux jobs », « attente des workers », « worker X terminé », « arrêt terminé ».
- Durées mesurables : combien de temps dure l’arrêt ? Quelle phase consomme du temps ? Souvent une mesure de temps monotone suffit, comme GetTickCount64 ou TStopwatch (monotone = non déformée par les modifications de l’heure système).
Si vous n Stopping , le d bst sur le terrain reste un jeu de devinettes. En exploitation de services, les logs sont souvent la seule chose que vous obtenez sans interaction.
Quels logs sont r glement utilis les dans les services r ?
- PID du service, heure de d marrage, version/build (sans surcharge excessive).
- Nombre de workers actifs, nombre de jobs en cours.
- D externes actives : “DB-Call en cours“, „HTTP-Request en cours“, „flush de fichier en cours“ (uniquement agr g g , pas chaque d tail).
- Timeout d’arr t atteint : quels workers sont encore actifs ?
D bogage sur le terrain : rendre reproductible au lieu de deviner
Les probl mes d’arr t surviennent souvent uniquement en production : charge diff rente, latences diff rentes, droits diff rents, fen tres de patch diff rentes. Quelques leviers prouv s en pratique :
Tester le service sous contr le
- Arr t pendant un traitement actif (pas au repos).
- Arr t lors d’une perturbation externe : DB bri vement inaccessible, endpoint HTTP lent, partage de fichiers indisponible.
- Arr t imm diatement apr s le d marrage (conditions de course : workers encore en cours d’initialisation).
Signaux de l’Event Viewer et du Service Control Manager
Windows consigne des v nements de service, mais ils sont souvent sommaires. Il est préférable que votre service crive lui-m me dans un fichier de log ou dans l’Event Log Windows. Important : la journalisation doit encore fonctionner pendant le processus d’arr t. Si vous lib rez le logger trop t t lors du shutdown ou si le flush est bloqu , vous perdez pr cis ment les traces cruciales.
Rendre visibles les threads bloqu s
Si vous constatez plusieurs reprises un «timeout d’arr t», examinez les tats des threads (p. ex. via un debugger/procdump en environnement de test). Vous trouverez souvent un thread en attente sur un handle qui n’est jamais signal , ou dans un appel r seau sans d lai d’attente. La correction n’est alors rarement «encore plus de sleep», mais un chemin d’arr t propre.
Quand l’effort en vaut-il vraiment la peine ?
Un service minimaliste, qui n’a qu’une simple minuterie et aucune d pendance externe, peut parfois «s’arr ter simplement». Mais d s qu’une des conditions suivantes s’applique, un arr t gracieux (graceful shutdown) propre vaut presque toujours la peine :
- Le service traite des jobs avec effets de bord ( criture de fichiers, transactions DB, appels API).
- Il y a plusieurs threads ou un pool.
- Le service d pend de ressources r seau (DB, REST, broker de messages, partages de fichiers).
- L’exploitation exige des fen tres de maintenance planifiables (reboots, mises jour, basculement).
La valeur ajout e9e n’est pas «l’
Étape suivante
Lorsque le sujet devient un projet réel, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.
Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.
- L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
- REST, l’accès aux données, les portails et le déploiement ne sont pas reportés à des phases ultérieures.
- Vous identifiez tôt quelle voie est viable économiquement et opérationnellement.