Comment traiter le signal SIGTERM avec élégance?

197

Supposons que nous ayons un démon aussi trivial écrit en python:

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

mainloop()

et nous le démonisons en utilisant start-stop-daemonce qui, par défaut, envoie le signal SIGTERM( TERM) --stop.

Supposons que l'étape en cours soit #2. Et en ce moment même, nous envoyons un TERMsignal.

Ce qui se passe, c'est que l'exécution se termine immédiatement.

J'ai trouvé que je peux gérer l'événement signal en utilisant, signal.signal(signal.SIGTERM, handler)mais le fait est qu'il interrompt toujours l'exécution en cours et passe le contrôle à handler.

Donc, ma question est - est-il possible de ne pas interrompre l'exécution en cours mais de gérer le TERMsignal dans un thread séparé (?) Afin que je puisse régler de shutdown_flag = Truemanière à mainloop()avoir une chance de s'arrêter gracieusement?

zerkms
la source
2
J'ai fait ce que vous demandez avant en utilisant signalfdet en masquant la livraison du SIGTERMprocessus.
Eric Urban

Réponses:

278

Une solution propre à utiliser basée sur la classe:

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
la source
1
Merci pour l'idée! J'ai utilisé une approche modifiée dans reboot-guard. github.com/ryran/reboot-guard/blob/master/rguard#L284:L304
rsaw
7
C'est la meilleure réponse (pas de threads requis) et devrait être l'approche privilégiée du premier essai.
jose.angel.jimenez
2
@ Mausy5043 Python vous permet de ne pas avoir de parenthèses pour définir les classes. Bien que ce soit parfait pour python 3.x, mais pour python 2.x, la meilleure pratique consiste à utiliser "class XYZ (object):". Raison étant: docs.python.org/2/reference/datamodel.html#newstyle
Mayank Jaiswal
2
Suivi, pour vous garder motivé, merci. Je l'utilise tout le temps.
chrisfauerbach
2
Dans le pire des cas, cela signifierait simplement faire une autre itération avant de s'arrêter gracieusement. La Falsevaleur n'est définie qu'une seule fois, puis elle ne peut passer de False à True que l'accès multiple n'est pas un problème.
Alceste_
52

Tout d'abord, je ne suis pas certain que vous ayez besoin d'un deuxième fil pour définir le shutdown_flag.
Pourquoi ne pas le définir directement dans le gestionnaire SIGTERM?

Une alternative est de lever une exception du SIGTERMgestionnaire, qui sera propagée dans la pile. En supposant que vous ayez une gestion des exceptions appropriée (par exemple avec with/ contextmanageret des try: ... finally:blocs), cela devrait être un arrêt assez gracieux, similaire à celui de Ctrl+Cvotre programme.

Exemple de programme 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"

Maintenant, voyez le Ctrl+Ccomportement:

$ ./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

Cette fois je l'envoie SIGTERMaprès 4 itérations avec 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

Cette fois, j'active mon SIGTERMgestionnaire personnalisé et je l'envoie SIGTERM:

$ ./signals-test.py handle_signal
Hello
Iteration #1
Iteration #2
Iteration #3
Iteration #4
Goodbye
$ echo $?
0
Will Manley
la source
3
"Pourquoi ne pas le définir directement dans le gestionnaire SIGTERM" --- parce que le thread de travail s'interromprait à un endroit aléatoire. Si vous mettez plusieurs instructions dans votre boucle de travail, vous verrez que votre solution met fin à un travailleur sur une position aléatoire, ce qui laisse le travail dans un état inconnu.
zerkms
Fonctionne bien pour moi, également dans un contexte Docker. Merci!
Marian
4
Si vous venez de définir un drapeau et de ne pas lever d'exception, ce sera la même chose qu'avec le thread. Il est donc superflu d'utiliser du fil ici.
Suor
28

Je pense que vous êtes proche d'une solution possible.

Exécutez mainloopdans un thread séparé et étendez-le avec la propriété shutdown_flag. Le signal peut être capturé signal.signal(signal.SIGTERM, handler)dans le fil principal (pas dans un fil séparé). Le gestionnaire de signal doit être défini shutdown_flagsur True et attendre la fin du thread avecthread.join()

moliware
la source
4
Oui, un fil séparé est la façon dont je l'ai finalement résolu, merci
zerkms
7
Les threads ne sont pas requis ici. Dans un programme unique, vous pouvez d'abord enregistrer un gestionnaire de signaux (l'enregistrement d'un gestionnaire de signaux n'est pas bloquant), puis écrire la boucle principale. La fonction de gestion du signal doit définir un indicateur lorsque la boucle doit vérifier cet indicateur. J'ai collé une solution basée sur la classe pour la même chose ici .
Mayank Jaiswal
2
Aucun moyen d'avoir un deuxième thread n'est nécessaire. Enregistrez le gestionnaire de signaux.
oneloop
page utile: g-loaded.eu/2016/11/24/…
Kamil Sindi
26

Voici un exemple simple sans threads ni classes.

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
réflexion
la source
11

Sur la base des réponses précédentes, j'ai créé un gestionnaire de contexte qui protège de sigint et 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
la source
4

Trouvé le moyen le plus simple pour moi. Voici un exemple avec fork pour plus de clarté que cette façon est utile pour le contrôle de flux.

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
la source
0

La solution la plus simple que j'ai trouvée, en s'inspirant des réponses ci-dessus, c'est

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
la source