python daemon sigterm start-stop-daemon

python - Cómo procesar la señal SIGTERM con gracia?



daemon start-stop-daemon (5)

Supongamos que tenemos un daemon tan trivial escrito en python:

def mainloop(): while True: # 1. do # 2. some # 3. important # 4. job # 5. sleep mainloop()

y lo demonizamos utilizando start-stop-daemon que por defecto envía la señal SIGTERM ( TERM ) en " --stop .

Supongamos que el paso actual realizado es #2 . Y en este mismo momento estamos enviando señal TERM .

Lo que sucede es que la ejecución termina inmediatamente.

Descubrí que puedo manejar el evento de señal usando signal.signal(signal.SIGTERM, handler) pero la cosa es que aún interrumpe la ejecución actual y pasa el control al handler .

Entonces, mi pregunta es: ¿es posible no interrumpir la ejecución actual, sino manejar la señal TERM en un hilo separado (?) Para poder configurar shutdown_flag = True para que mainloop() tuviera la oportunidad de detenerse con gracia?


Aquí hay un ejemplo simple sin hilos o clases.

import signal run = True def handler_stop_signals(signum, frame): global run run = False signal.signal(signal.SIGINT, handler_stop_signals) signal.signal(signal.SIGTERM, handler_stop_signals) while run: pass # do stuff including other IO stuff


Basado en las respuestas anteriores, he creado un administrador de contexto que protege de sigint y sigterm.

import logging import signal import sys class TerminateProtected: """ Protect a piece of code from being killed by SIGINT or SIGTERM. It can still be killed by a force kill. Example: with TerminateProtected(): run_func_1() run_func_2() Both functions will be executed even if a sigterm or sigkill has been received. """ killed = False def _handler(self, signum, frame): logging.error("Received SIGINT or SIGTERM! Finishing this block, then exiting.") self.killed = True def __enter__(self): self.old_sigint = signal.signal(signal.SIGINT, self._handler) self.old_sigterm = signal.signal(signal.SIGTERM, self._handler) def __exit__(self, type, value, traceback): if self.killed: sys.exit(0) signal.signal(signal.SIGINT, self.old_sigint) signal.signal(signal.SIGTERM, self.old_sigterm) if __name__ == ''__main__'': print("Try pressing ctrl+c while the sleep is running!") from time import sleep with TerminateProtected(): sleep(10) print("Finished anyway!") print("This only prints if there was no sigint or sigterm")


Creo que estás cerca de una posible solución.

Ejecute mainloop en un hilo separado y mainloop con la propiedad shutdown_flag . La señal se puede capturar con signal.signal(signal.SIGTERM, handler) en el hilo principal (no en un hilo separado). El manejador de señal debe establecer shutdown_flag en True y esperar a que el subproceso finalice con thread.join()


En primer lugar, no estoy seguro de que necesite un segundo hilo para configurar shutdown_flag. ¿Por qué no configurarlo directamente en el controlador SIGTERM?

Una alternativa es generar una excepción desde el controlador SIGTERM , que se propagará por la pila. Asumiendo que tienes un manejo de excepciones adecuado (por ejemplo, con / contextmanager y try: ... finally: bloques) esto debería ser un cierre bastante elegante, similar a si tuvieras que Ctrl-C tu programa.

Ejemplo de programa signals-test.py :

#!/usr/bin/python from time import sleep import signal import sys def sigterm_handler(_signo, _stack_frame): # Raises SystemExit(0): sys.exit(0) if sys.argv[1] == "handle_signal": signal.signal(signal.SIGTERM, sigterm_handler) try: print "Hello" i = 0 while True: i += 1 print "Iteration #%i" % i sleep(1) finally: print "Goodbye"

Ahora vea el comportamiento de Ctrl-C:

$ ./signals-test.py default Hello Iteration #1 Iteration #2 Iteration #3 Iteration #4 ^CGoodbye Traceback (most recent call last): File "./signals-test.py", line 21, in <module> sleep(1) KeyboardInterrupt $ echo $? 1

Esta vez lo envío SIGTERM después de 4 iteraciones con kill $(ps aux | grep signals-test | awk ''/python/ {print $2}'') :

$ ./signals-test.py default Hello Iteration #1 Iteration #2 Iteration #3 Iteration #4 Terminated $ echo $? 143

Esta vez habilito mi controlador SIGTERM personalizado y lo envío SIGTERM :

$ ./signals-test.py handle_signal Hello Iteration #1 Iteration #2 Iteration #3 Iteration #4 Goodbye $ echo $? 0


Una solución limpia basada en clases para usar:

import signal import time class GracefulKiller: kill_now = False def __init__(self): signal.signal(signal.SIGINT, self.exit_gracefully) signal.signal(signal.SIGTERM, self.exit_gracefully) def exit_gracefully(self,signum, frame): self.kill_now = True if __name__ == ''__main__'': killer = GracefulKiller() while True: time.sleep(1) print("doing something in a loop ...") if killer.kill_now: break print "End of the program. I was killed gracefully :)"