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

197

Supongamos que tenemos un demonio tan trivial escrito en python:

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

mainloop()

y lo damonizamos usando start-stop-daemonque por defecto envía señal SIGTERM( TERM) --stop.

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

Lo que sucede es que la ejecución termina de inmediato.

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

Entonces, mi pregunta es: ¿es posible no interrumpir la ejecución actual sino manejar la TERMseñal en un hilo separado (?) Para poder configurarlo shutdown_flag = Truey mainloop()tener la oportunidad de detenerlo con gracia?

zerkms
fuente
2
Hice lo que está pidiendo antes al usar signalfdy enmascarar la entrega del SIGTERMproceso.
Eric Urban

Respuestas:

278

Una solución limpia para usar basada en la clase:

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 not killer.kill_now:
    time.sleep(1)
    print("doing something in a loop ...")

  print("End of the program. I was killed gracefully :)")
Mayank Jaiswal
fuente
1
Gracias por la idea! Usé un enfoque modificado en reiniciar-guardia. github.com/ryran/reboot-guard/blob/master/rguard#L284:L304
rsaw
77
Esta es la mejor respuesta (no se requieren hilos), y debería ser el enfoque preferido para el primer intento.
jose.angel.jimenez
2
@ Mausy5043 Python le permite no tener paréntesis para definir clases. Aunque está perfectamente bien para python 3.x, pero para python 2.x, la mejor práctica es usar "clase XYZ (objeto):". La razón es: docs.python.org/2/reference/datamodel.html#newstyle
Jaiswal
2
Seguimiento, para mantenerte motivado, gracias. Uso esto todo el tiempo.
chrisfauerbach
2
En el peor de los casos, eso simplemente significaría hacer otra iteración antes de cerrar con gracia. El Falsevalor se establece solo una vez, y luego solo puede pasar de False a True para que el acceso múltiple no sea un problema.
Alceste_
52

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

Una alternativa es generar una excepción desde el SIGTERMcontrolador, que se propagará por la pila. Suponiendo que tiene el manejo adecuado de excepciones (por ejemplo, con with/ contextmanagery try: ... finally:bloques), este debería ser un cierre bastante elegante, similar a si estuviera en Ctrl+Csu programa.

Programa de ejemplo 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 Ctrl+Ccomportamiento:

$ ./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 SIGTERMdespué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 SIGTERMcontrolador personalizado y lo envío SIGTERM:

$ ./signals-test.py handle_signal
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Goodbye
$ echo $?
0
Will Manley
fuente
3
"¿Por qué no configurarlo directamente en el controlador SIGTERM" --- porque el subproceso de trabajo se interrumpiría en un lugar aleatorio. Si coloca varias declaraciones en su ciclo de trabajo, verá que su solución termina a un trabajador en una posición aleatoria, lo que deja el trabajo en un estado desconocido.
zerkms
Funciona bien para mí, también en un contexto Docker. ¡Gracias!
Marian
44
Si solo establece una bandera y no genera una excepción, será lo mismo que con el hilo. Entonces, usar hilo es superfluo aquí.
Suor
28

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

Ejecute mainloopen un hilo separado y extiéndalo con la propiedad shutdown_flag. La señal puede capturarse signal.signal(signal.SIGTERM, handler)en el hilo principal (no en un hilo separado). El controlador de señal debe establecerse shutdown_flagen True y esperar a que el hilo termine conthread.join()

moliware
fuente
44
Sí, un hilo separado es cómo finalmente lo resolví, gracias
zerkms
77
No se requieren hilos aquí. En un solo programa de subprocesos, primero puede registrar un controlador de señal (registrar un controlador de señal no es de bloqueo) y luego escribir mainloop. La función de controlador de señal debe establecer una marca cuando y el bucle debe verificar esta marca. He pegado una solución basada en clases para lo mismo aquí .
Mayank Jaiswal
2
De ninguna manera es necesario tener un segundo hilo. Registrar el manejador de señal.
oneloop
página útil: g-loaded.eu/2016/11/24/…
Kamil Sindi
26

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
aunque estrella
fuente
11

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")
Okke
fuente
4

Encontré la manera más fácil para mí. Aquí un ejemplo con fork para aclarar que de esta manera es útil para el control de flujo.

import signal
import time
import sys
import os

def handle_exit(sig, frame):
    raise(SystemExit)

def main():
    time.sleep(120)

signal.signal(signal.SIGTERM, handle_exit)

p = os.fork()
if p == 0:
    main()
    os._exit()

try:
    os.waitpid(p, 0)
except (KeyboardInterrupt, SystemExit):
    print('exit handled')
    os.kill(p, 15)
    os.waitpid(p, 0)
Kron
fuente
0

La solución más simple que he encontrado, inspirándome en las respuestas anteriores es

class SignalHandler:

    def __init__(self):

        # register signal handlers
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)

        self.logger = Logger(level=ERROR)

    def exit_gracefully(self, signum, frame):
        self.logger.info('captured signal %d' % signum)
        traceback.print_stack(frame)

        ###### do your resources clean up here! ####

        raise(SystemExit)
loretoparisi
fuente