Del tema de la revista a la práctica del proyecto
Páginas de servicios y técnicas relacionadas
Un Windows Service in Delphi suele pasar desapercibido en la operación diaria: se ejecuta en segundo plano, procesa trabajos, escribe logs y se comunica con bases de datos o APIs REST. Hasta que alguien hace clic en „Detener servicio“ —o toca un reinicio por parche— y el servicio no se detiene de forma limpia. Entonces la consola de servicios muestra durante minutos „Deteniéndose…“, el servicio queda colgado en el estado Stop Pending, y en el peor de los casos el proceso es terminado a la fuerza. Precisamente aquí merece la pena tratar el Windows Service in Delphi Graceful Shutdown como un tema arquitectónico: con una señal de apagado clara, timeouts definidos y hilos que realmente reaccionen.
En este artículo no se trata de los internos de frameworks, sino de un patrón aplicable en la práctica: TEvent como señal de parada (un objeto de sincronización a nivel de kernel de System.SyncObjs), combinado con una estrategia de Stop-Timeout que considere tanto al Windows Service Control Manager (SCM, es decir, el componente Windows que arranca/detiene servicios) como a tus propios hilos de trabajo. Además, casos límite típicos, enfoques de depuración y la cuestión de cuándo realmente compensa la lógica adicional.
Windows Service in Delphi Graceful Shutdown en la práctica
La causa más frecuente es sencilla: el servicio tiene al menos un hilo que está en una operación bloqueante y no conoce una vía de cancelación. Clásicos:
- Bucles de sondeo con Sleep: „while not Terminated do Sleep(1000)“. Al detener, la señal llega, pero el hilo no reacciona hasta pasados hasta 1 segundo (o 30 segundos…).
- E/S bloqueantes: llamadas a bases de datos, peticiones HTTP, Named Pipes, esperas del sistema de archivos — todo lo que „simplemente espera“ sin atender una señal de parada.
- Consumidor de colas sin wakeup: un worker espera en una cola, pero al detener no se le despierta para salir.
- Orden de locks / deadlocks: al detener se realiza el „Cleanup“ mientras otros hilos aún mantienen locks. Esto suele aparecer solo en la ruta de parada, porque el orden allí difiere del funcionamiento normal.
El Windows SCM espera que un servicio responda con rapidez a un comando de parada y reporte continuamente su estado (a través de SetServiceStatus; Delphi lo encapsula en el componente de servicio). Si aceptas el evento de parada pero no finalizas correctamente tus hilos, el proceso permanece vivo —y Windows decidirá en algún momento que „tarda demasiado“. El resultado será o bien una terminación forzada o un servicio que queda atascado en un estado intermedio indeterminado.
Principio básico: una señal de parada que entienda cada hilo de trabajo
Un Graceful Shutdown solo funciona si dispones de una señal que:
- pueda ser observada por todos los hilos relevantes,
- también tenga efecto incluso desde estados de espera bloqueantes,
- sea en el Stop-Pfad determinista (sin la esperanza de «quizá salga en algún momento»),
- tenga una estrategia de timeout clara.
En Delphi TEvent es una herramienta muy usable para ello: un objeto Event que internamente se implementa sobre Windows-handles (comparable a CreateEvent/SetEvent). Puedes usarlo como señal de «Stop requested». Cada Worker no espera ciegamente, sino que espera «trabajo o Stop».
Elegir TEvent correctamente: ManualReset vs. AutoReset
Para señales de Stop normalmente quieres Manual Reset (reseteo manual): una vez puesto, el Event permanece «signaled» hasta que lo reseteas. De ese modo se asegura que cualquier hilo que entre más tarde en una fase de espera igualmente reconozca la señal de Stop. Auto Reset sería arriesgado aquí, porque restablece la señal automáticamente tras un hilo esperando y otros hilos podrían perder la señal de Stop.
Delphi-Service-Lebenszyklus: Dónde llega realmente el Stop
Un Delphi-Windows- und Linux-Services se basa típicamente en TService (VCL/RTL). El SCM envía comandos (Start, Stop, Pause, Continue). Delphi invoca entonces los eventos/métodos correspondientes (según la plantilla, p. ej. OnStart, OnStop, OnExecute).
Importante para la arquitectura:
- OnStop no es un lugar para esperar largo tiempo sin actualizaciones de estado. Es el lugar donde inicias el apagado y luego esperas de forma controlada — con timeout.
- OnExecute suele ser un bucle. Si allí trabajas «sin fin», el bucle debe reaccionar a una señal de Stop.
- Worker-Threads (TThread oder Thread-Pools) deben reaccionar a la misma señal de Stop; de lo contrario el servicio queda parado lógicamente, pero físicamente aún no ha terminado.
Patrón limpio: Stop-Event + Join de los workers + fallback forzado
El patrón práctico consta de cuatro pasos:
- Solicitar Stop: establecer el Stop-Event, no aceptar más jobs.
- Desencadenar wakeups: si los workers esperan en colas o en sleeps, deben poder «despertar» (p. ej. mediante señal de Event/Queue).
- Finalizar ordenadamente: los workers terminan sus bucles, cierran recursos (conexiones DB, archivos, handles) y reportan «listo».
- Timeout y fallback: si no todo termina a tiempo, debes tomar una decisión: seguir esperando (con actualización de estado) o abortar/controlar el cierre forzado (según el riesgo).
La clave es: ningún thread debe esperar exclusivamente por tiempo (Sleep) ni bloquearse exclusivamente por I/O sin considerar en paralelo una señal de Stop. En su lugar, usa funciones de espera que contemplen múltiples señales (p. ej. «Stop-Event o Work-Event»), o encapsula la I/O en timeouts más comprobaciones de Stop.
Pensar correctamente el Stop-Timeout: SCM-Timeout vs. timeout propio de shutdown
Aquí ocurren la mayoría de los malentendidos en proyectos. Existen dos niveles distintos de timeout:
- Expectativa del SCM: Windows espera que, estando en el estado SERVICE_STOP_PENDING, informes periódicamente del progreso. De lo contrario parece que te has quedado colgado. Delphi se encarga de eso en parte, pero en cuanto tú mismo quedes bloqueado más tiempo necesitas una estrategia para seguir permitiendo actualizaciones de estado (o para mantener breve tu fase de detención).
- Tu propio Shutdown-Timeout: Defines p. ej. „Nos damos 20 Sekunden, um laufende Jobs sauber abzuschließen, dann brechen wir ab.“ Es una decisión de arquitectura: consistencia de datos vs. obligación de reinicio vs. requisitos operativos.
En la práctica eso significa: tu servicio debería pasar rápidamente a un estado en el que no inicie nuevas unidades de trabajo y luego solo espere a que termine el trabajo en curso, pero no de forma indefinida. Y esta fase de espera debería ejecutarse en intervalos cortos, para que puedas reaccionar y, si procede, registrar información.
¿Cuánto puede durar la detención?
No existe un número mágico que sirva siempre. Para muchos servicios de negocio un rango objetivo de 5–30 Sekunden es realista: tiempo suficiente para datos «in-flight», pero lo bastante corto para ventanas de parcheo. Si de forma habitual necesitas más tiempo, suele indicar que procesas unidades demasiado grandes de una vez o que dependencias externas (DB/HTTP) funcionan sin timeout.
Implementación con TEvent: una estructura que se mantiene estable en producción
Una arquitectura probada en el servicio Delphi se ve así (sin agotar los detalles del framework):
- Un Stop-Event (TEvent, Manual Reset) que se establece al detener.
- Uno o varios Worker-Threads que en su bucle principal comprueban periódicamente si se ha solicitado detenerse.
- Opcionalmente un Work-Event o una cola que señale trabajo. Los workers esperan entonces por „Work oder Stop“.
- Una fase de Shutdown que realiza „join“ a los workers (es decir, espera a que terminen), pero con timeout.
Lo decisivo no es si usas TThread, omnithreadlibrary o un pool propio, sino que tus workers no funcionen „a ciegas“. Un bucle de worker debería estructurarse así: esperar eventos → trabajo en pequeños trozos → comprobar Stop entre trozos → liberar recursos limpiamente.
Trampa: Terminate por sí solo no basta
Muchos hilos de Delphi se „terminan“ con Terminate. Pero eso es solo una bandera. Si el hilo está en una API bloqueante, inicialmente no ocurre nada. Por eso un Stop-Event propio es tan útil: puedes integrarlo en llamadas de espera y provocar wakeups específicos.
Trampa: FreeOnTerminate en contexto de servicio
En servicios frecuentemente se ve FreeOnTerminate := True. Eso puede funcionar, pero complica el control del shutdown, porque a menudo ya no tienes una referencia limpia para esperar al fin del hilo y registrar estados de error. Para una lógica de parada controlada suele ser más estable poseer los threads de forma explícita y, en el shutdown, esperar y liberar de forma determinista.
Operaciones bloqueantes: así las haces interrumpibles
La parte complicada no es el evento en sí, sino los puntos en los que tu servicio se bloquea. Tres clases típicas:
1) Reemplazar Sleep/Polling: Wait con Stop-Event
Si trabajas de forma periódica („comprobar cada 10 segundos“), no uses Sleep(10000); espera en su lugar a un evento con timeout. Entonces tu Stop-Event podrá terminar la espera de inmediato. Eso reduce la latencia de Stop y evita la sensación „el servicio no responde“.
2) Queue-Consumer: combinar Work-Event y Stop-Event
Si tienes una arquitectura Producer/Consumer (p. ej. los jobs se colocan en una cola), necesitas una señal que despierte a los consumidores. A menudo es otro TEvent (Work available). El consumidor espera entonces en dos handles: „Work“ o „Stop“. Al hacer Stop señalas el Stop-Event y, si procede, también el Work-Event, para que todos los consumidores salgan garantizados del Wait.
3) Llamadas externas (DB/HTTP): timeouts y rutas de cancelación
En accesos a bases de datos o llamadas HTTP se decide si tu servicio se detiene correctamente. En operación aplica: Ninguna llamada sin timeout. Un timeout no es un lujo, sino un requisito para poder controlarlo. Además deberías comprobar el Stop entre fases de retries/backoff. Si no, tienes el clásico: „el servicio no se detiene porque está haciendo 10 retries con Sleep“.
En algunas bibliotecas puedes disparar cancelaciones explícitas (p. ej. abortar una consulta). Si eso no es posible, al menos configura timeouts lo bastante cortos como para no sobrepasar el timeout de shutdown.
Stop Pending correcto: Estado, Logging y gestión de expectativas
Cuando un servicio se detiene, desde el punto de vista operativo es importante entender dónde se queda colgado. Para ello necesitas dos cosas:
- Marcadores de log en la ruta de Stop: „Stop solicitado“, „no hay nuevos jobs“, „esperando a Worker“, „Worker X terminado“, „Shutdown completado“.
- Tiempos medibles: ¿Cuánto tarda el Stop? ¿Qué fase consume tiempo? A menudo basta una medida de tiempo monótona como GetTickCount64 o TStopwatch (monótona = no distorsionada por cambios de la hora del sistema).
Si en la ruta de parada solo escribes una entrada de log „Stopping…“, la depuración en campo seguirá siendo un juego de adivinanzas. En la operación del servicio, los logs son a menudo lo único que obtienes sin intervención.
¿Qué logs son realmente útiles en los servicios?
- PID del servicio, hora de inicio, versión/build (sin sobrecarga excesiva).
- Número de Worker activos, número de jobs en curso.
- Dependencias externas activas: „DB-Call läuft“, „HTTP-Request läuft“, „Datei-Flush läuft“ (solo agregadas, no cada detalle).
- Stop-Timeout erreicht: ¿qué Worker siguen abiertos?
Depuración en campo: hacer reproducible en lugar de adivinar
Los problemas de parada suelen aparecer solo en producción: distinta carga, distintas latencias, distintos permisos, distintas ventanas de parcheo. Algunos enfoques probados en la práctica:
Probar el servicio bajo control
- Detención durante procesamiento activo (no en idle).
- Detención durante una falla externa: DB brevemente no disponible, endpoint HTTP lento, Fileshare desaparecido.
- Detención inmediatamente después del arranque (race-conditions: Worker aún en inicialización).
Visor de eventos y señales del Service Control Manager
Windows escribe eventos de servicio, pero suelen ser toscos. Es mejor que tu servicio escriba por sí mismo en un archivo de log o en el Event Log de Windows. Lo importante: el logging debe seguir funcionando en la ruta de parada. Si liberas el logger demasiado pronto durante el apagado o el flush queda bloqueado, perderás exactamente las trazas decisivas.
Hacer visibles los hilos colgados
Si ves repetidamente „Stop Timeout“, vale la pena revisar los estados de los hilos (p. ej. con debugger/Procdump en un entorno de prueba). A menudo encontrarás un hilo en estado de espera sobre un handle que nunca se señaliza, o en una llamada de red sin timeout. La solución rara vez es „noch mehr Sleep“, y suele ser un camino de abortado limpio.
¿Cuándo merece la pena el esfuerzo realmente?
Un servicio minimalista que solo tiene un temporizador y ninguna dependencia externa puede a veces „detenerse simplemente“. Sin embargo, en cuanto se cumple alguna de las siguientes condiciones, merece la pena un apagado ordenado casi siempre:
- El servicio procesa jobs con efectos secundarios (escritura de archivos, transacciones DB, llamadas a API).
- Hay varios hilos o un pool.
- El servicio depende de recursos de red (DB, REST, message broker, fileshares).
- La operación exige ventanas de mantenimiento planificables (reinicios, actualizaciones, failover).
El valor añadido no es „Eleganz“, sino la seguridad operacional: menos abortos duros de procesos, menos estados intermedios inconsistentes, menos intervenciones manuales.
Puntos problemáticos en la práctica: qué suele fallar durante el apagado
1) Se establece la parada, pero siguen entrando nuevos jobs
Si aceptas trabajo entrante (p. ej. por socket, trigger de archivo, Timer), debes en la ruta de parada detener primero la aceptación de trabajo nuevo: Listener schließen, Timer deaktivieren, Scheduler anhalten. Si no, perseguirás el fin porque seguirán iniciándose nuevos jobs.
2) Cleanup bloqueado (Flush, Close, Finalize)
„Solo un flush rápido“ puede ser peligroso en el contexto de servicios, si el destino (unidad de red, log remoto, DB) está colgado. Por eso: limpieza sí, pero con tiempo limitado. Si hace falta, deberás decidir qué datos pierdes en memoria en lugar de bloquear por completo la parada.
3) Bloqueos y orden
Al detenerse accedes con frecuencia a las mismas estructuras de datos que los Worker (Queues, Caches, States). Si el hilo de parada mantiene Locks y luego espera al fin de los Worker, mientras éstos necesitan ese mismo Lock, se produce un Stop-Deadlock. Contramedidas: mantener los tiempos de retención del Lock cortos, no «esperar bajo Lock» en la ruta de parada, definir un orden claro.
4) Concurrencia en un Stop duplicado
En la práctica el Stop puede dispararse varias veces (p. ej. Stop + Shutdown, o Stop vuelve a ocurrir). Tu ruta de Stop debe ser idempotente: establecer el evento de parada está bien, pero la lógica doble de Join/Free debe protegerse correctamente (p. ej. mediante un flag atómico).
Visión operativa: qué esperan los administradores y responsables de TI del servicio
Para operación y administración, al final no importa cuán «bonito» sea el código, sino si el servicio:
- al Stop finaliza de forma fiable (planificable, sin bloqueos),
- al Stop no produce datos inconsistentes (p. ej. archivos incompletos, transacciones abiertas),
- en caso de error proporciona logs útiles,
- en ventanas de mantenimiento y despliegues es predecible.
Esta es también la razón por la cual el tema del Stop-Timeout no es solo «cosa de desarrolladores»: influye en los ciclos de parcheo, los tiempos de recuperación y en si los despliegues automatizados son factibles.
Pautas concretas para un diseño de Shutdown robusto
Si quieres estandarizar el tema de forma pragmática, estas pautas han demostrado ser eficaces:
- Un evento global de Stop, Manual Reset, creado temprano en el ciclo de vida del servicio, liberado al final.
- No usar Sleep en bucles de Worker sin alternativa que responda al Stop (usar Wait con timeout).
- Todas las llamadas externas con timeouts (DB, HTTP, fileshares). Elegir los timeouts de modo que encajen en tu shutdown-timeout.
- Stop-Timeout como configuración (p. ej. en INI/Registry), para que Operación pueda reaccionar sin recompilar.
- Modelo por fases: primero graceful (permitir que los trabajos en curso terminen), luego opcionalmente «soft abort» (no iniciar nuevos pasos), y finalmente una salida dura como último recurso.
- Registros de Stop claros con fases y medición de tiempos.
Conclusión: TEvent + Stop-Timeout no es un lujo, sino capacidad de control
Un Stop colgado rara vez es un fallo aislado: suele ser un agujero de arquitectura: trabajo ejecutándose en threads o en llamadas bloqueantes que no conocen una señal común de parada. Con un evento de Stop claro (TEvent, Manual Reset), waits que respondan al Stop en lugar de Sleep, timeouts consistentes para dependencias externas y un shutdown-timeout definido, obtendrás un servicio predecible en la operación diaria.
Esta inversión de código merece la pena especialmente si tu servicio opera en entornos de producción con ventanas de mantenimiento, despliegues automatizados o efectos secundarios críticos. Entonces el «Graceful Shutdown» no es cosmética, sino un componente para una operación estable y menos escaladas en el próximo reinicio.
Si quieres establecer correctamente vuestro Stop-Pfad o revisar un servicio Delphi existente respecto a lógica de shutdown robusta y seguridad operativa, una llamada técnica de sparring suele ser la vía más rápida para definir medidas claras: ponerse en contacto.
Para este tema también son importantes Delphi Windows Service y Tevent Delphi. El artículo sitúa estos aspectos de forma comprensible y muestra qué importa en la operación cotidiana.
Discutir proyecto o iniciativa de modernización con Net-Base.
siguiente paso
Cuando un tema se convierte en un proyecto real, arquitectura, entorno existente y operación deben considerarse conjuntamente desde el inicio.
No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.
- La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
- REST, el acceso a datos, los portales y el despliegue no se relegan a fases posteriores.
- Usted detecta con antelación qué camino es viable, tanto económica como operativamente.