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 :)"