web-dev-qa-db-fra.com

Python Multiprocessing: Gestion des erreurs enfants dans le parent

Je joue actuellement avec le multitraitement et les files d'attente. J'ai écrit un morceau de code pour exporter les données de mongoDB, les mapper dans une structure relationnelle (plate), convertir toutes les valeurs en chaîne et les insérer dans mysql.

Chacune de ces étapes est soumise en tant que processus et reçoit des files d'attente d'importation/exportation, sans danger pour l'exportation mongoDB qui est gérée dans le parent.

Comme vous le verrez ci-dessous, j'utilise des files d'attente et les processus enfants se terminent lorsqu'ils lisent "Aucun" dans la file d'attente. Le problème que j'ai actuellement est que, si un processus enfant s'exécute dans une exception non gérée, cela n'est pas reconnu par le parent et le reste continue simplement à fonctionner. Ce que je veux arriver, c'est que tout le Shebang s'arrête et, au mieux, relance l'erreur de l'enfant.

J'ai deux questions:

  1. Comment détecter l'erreur enfant dans le parent?
  2. Comment puis-je tuer mes processus enfants après avoir détecté l'erreur (meilleure pratique)? Je me rends compte que mettre "Aucun" dans la file d'attente pour tuer l'enfant est assez sale.

J'utilise python 2.7.

Voici les parties essentielles de mon code:

# Establish communication queues
mongo_input_result_q = multiprocessing.Queue()
mapper_result_q = multiprocessing.Queue()
converter_result_q = multiprocessing.Queue()

[...]

    # create child processes
    # all processes generated here are subclasses of "multiprocessing.Process"

    # create mapper
    mappers = [mongo_relational_mapper.MongoRelationalMapper(mongo_input_result_q, mapper_result_q, columns, 1000)
               for i in range(10)]

    # create datatype converter, converts everything to str
    converters = [datatype_converter.DatatypeConverter(mapper_result_q, converter_result_q, 'str', 1000)
                  for i in range(10)]

    # create mysql writer
    # I create a list of writers. currently only one, 
    # but I have the option to parallellize it further
    writers = [mysql_inserter.MySqlWriter(mysql_Host, mysql_user, mysql_passwd, mysql_schema, converter_result_q
               , columns, 'w_'+mysql_table, 1000) for i in range(1)]

    # starting mapper
    for mapper in mappers:
        mapper.start()
    time.sleep(1)

    # starting converter
    for converter in converters:
        converter.start()

    # starting writer
    for writer in writers:
        writer.start()

[... initialisation de la connexion mongo db ...]

    # put each dataset read to queue for the mapper
    for row in mongo_collection.find({inc_column: {"$gte": start}}):
        mongo_input_result_q.put(row)
        count += 1
        if count % log_counter == 0:
            print 'Mongo Reader' + " " + str(count)
    print "MongoReader done"

    # Processes are terminated when they read "None" object from queue
    # now that reading is finished, put None for each mapper in the queue so they terminate themselves
    # the same for all followup processes
    for mapper in mappers:
        mongo_input_result_q.put(None)
    for mapper in mappers:
        mapper.join()
    for converter in converters:
        mapper_result_q.put(None)
    for converter in converters:
        converter.join()
    for writer in writers:
        converter_result_q.put(None)
    for writer in writers:
        writer.join()
36
drunken_monkey

Je ne connais pas la pratique standard mais ce que j'ai trouvé, c'est que pour avoir un multitraitement fiable, je conçois les méthodes/classe/etc. spécifiquement pour travailler avec le multitraitement. Sinon, vous ne savez jamais vraiment ce qui se passe de l'autre côté (sauf si j'ai manqué un mécanisme pour cela).

Plus précisément, je fais:

  • Sous-classe multiprocessing.Process ou créer des fonctions qui prennent spécifiquement en charge le multitraitement (encapsuler des fonctions que vous n'avez pas le contrôle si nécessaire)
  • toujours fournir une erreur partagée multiprocessing.Queue du processus principal à chaque processus de travail
  • placez le code d'exécution entier dans un try: ... except Exception as e. Ensuite, lorsque quelque chose d'inattendu se produit, envoyez un package d'erreur avec:
    • l'identifiant du processus qui est mort
    • l'exception avec son contexte d'origine ( cochez ici ). Le contexte d'origine est vraiment important si vous souhaitez enregistrer des informations utiles dans le processus principal.
  • bien sûr, gérer les problèmes attendus comme normal dans le cadre du fonctionnement normal du travailleur
  • (similaire à ce que vous avez déjà dit) en supposant un processus de longue durée, enveloppez le code en cours (à l'intérieur de try/catch-all) avec une boucle
    • définir un jeton d'arrêt dans la classe ou pour les fonctions.
    • Lorsque le processus principal souhaite que le ou les travailleurs s'arrêtent, envoyez simplement le jeton d'arrêt. pour arrêter tout le monde, envoyez suffisamment pour tous les processus.
    • la boucle d'emballage vérifie l'entrée q pour le jeton ou toute autre entrée que vous souhaitez

Le résultat final est des processus de travail qui peuvent survivre longtemps et qui peuvent vous permettre de savoir ce qui se passe en cas de problème. Ils mourront tranquillement car vous pouvez gérer tout ce que vous devez faire après l'exception fourre-tout et vous saurez également quand vous devez redémarrer un travailleur.

Encore une fois, je viens d'arriver à ce modèle par essais et erreurs, donc je ne sais pas à quel point il est standard. Est-ce que cela aide avec ce que vous demandez?

28
KobeJohn

Pourquoi ne pas laisser le Processus s’occuper de ses propres exceptions, comme ceci:

import multiprocessing as mp
import traceback

class Process(mp.Process):
    def __init__(self, *args, **kwargs):
        mp.Process.__init__(self, *args, **kwargs)
        self._pconn, self._cconn = mp.Pipe()
        self._exception = None

    def run(self):
        try:
            mp.Process.run(self)
            self._cconn.send(None)
        except Exception as e:
            tb = traceback.format_exc()
            self._cconn.send((e, tb))
            # raise e  # You can still rise this exception if you need to

    @property
    def exception(self):
        if self._pconn.poll():
            self._exception = self._pconn.recv()
        return self._exception

Maintenant, vous avez à la fois erreur et traceback:

def target():
    raise ValueError('Something went wrong...')

p = Process(target = target)
p.start()
p.join()

if p.exception:
    error, traceback = p.exception
    print traceback

Cordialement, Marek

21
mrkwjc

Grâce à kobejohn, j'ai trouvé une solution agréable et stable.

  1. J'ai créé une sous-classe de multiprocessing.Process qui implémente certaines fonctions et écrase la méthode run() pour envelopper une nouvelle méthode saferun dans un bloc try-catch. Cette classe nécessite un feedback_queue pour initialiser qui est utilisé pour rapporter les informations, le débogage et les messages d'erreur au parent. Les méthodes de journalisation de la classe sont des wrappers pour les fonctions de journalisation définies globalement du package:

    class EtlStepProcess(multiprocessing.Process):
    
    def __init__(self, feedback_queue):
        multiprocessing.Process.__init__(self)
        self.feedback_queue = feedback_queue
    
    def log_info(self, message):
        log_info(self.feedback_queue, message, self.name)
    
    def log_debug(self, message):
        log_debug(self.feedback_queue, message, self.name)
    
    def log_error(self, err):
        log_error(self.feedback_queue, err, self.name)
    
    def saferun(self):
        """Method to be run in sub-process; can be overridden in sub-class"""
        if self._target:
            self._target(*self._args, **self._kwargs)
    
    def run(self):
        try:
            self.saferun()
        except Exception as e:
            self.log_error(e)
            raise e
        return
    
  2. J'ai sous-classé toutes mes autres étapes de processus d'EtlStepProcess. Le code à exécuter est implémenté dans la méthode saferun () plutôt que d'être exécuté. De cette façon, je n'ai pas à ajouter un bloc try catch autour, car cela est déjà fait par la méthode run (). Exemple:

    class MySqlWriter(EtlStepProcess):
    
    def __init__(self, mysql_Host, mysql_user, mysql_passwd, mysql_schema, mysql_table, columns, commit_count,
                 input_queue, feedback_queue):
        EtlStepProcess.__init__(self, feedback_queue)
        self.mysql_Host = mysql_Host
        self.mysql_user = mysql_user
        self.mysql_passwd = mysql_passwd
        self.mysql_schema = mysql_schema
        self.mysql_table = mysql_table
        self.columns = columns
        self.commit_count = commit_count
        self.input_queue = input_queue
    
    def saferun(self):
        self.log_info(self.name + " started")
        #create mysql connection
        engine = sqlalchemy.create_engine('mysql://' + self.mysql_user + ':' + self.mysql_passwd + '@' + self.mysql_Host + '/' + self.mysql_schema)
        meta = sqlalchemy.MetaData()
        table = sqlalchemy.Table(self.mysql_table, meta, autoload=True, autoload_with=engine)
        connection = engine.connect()
        try:
            self.log_info("start MySQL insert")
            counter = 0
            row_list = []
            while True:
                next_row = self.input_queue.get()
                if isinstance(next_row, Terminator):
                    if counter % self.commit_count != 0:
                        connection.execute(table.insert(), row_list)
                    # Poison pill means we should exit
                    break
                row_list.append(next_row)
                counter += 1
                if counter % self.commit_count == 0:
                    connection.execute(table.insert(), row_list)
                    del row_list[:]
                    self.log_debug(self.name + ' ' + str(counter))
    
        finally:
            connection.close()
        return
    
  3. Dans mon fichier principal, je soumets un processus qui fait tout le travail et je lui donne un feedback_queue. Ce processus démarre toutes les étapes, puis relit à partir de mongoDB et place les valeurs dans la file d'attente initiale. Mon processus principal écoute la file d'attente de commentaires et imprime tous les messages du journal. S'il reçoit un journal des erreurs, il imprime l'erreur et met fin à son enfant, qui en retour met également fin à tous ses enfants avant de mourir.

    if __== '__main__':
    feedback_q = multiprocessing.Queue()
    p = multiprocessing.Process(target=mongo_python_export, args=(feedback_q,))
    p.start()
    
    while p.is_alive():
        fb = feedback_q.get()
        if fb["type"] == "error":
            p.terminate()
            print "ERROR in " + fb["process"] + "\n"
            for child in multiprocessing.active_children():
                child.terminate()
        else:
            print datetime.datetime.fromtimestamp(fb["timestamp"]).strftime('%Y-%m-%d %H:%M:%S') + " " + \
                                                  fb["process"] + ": " + fb["message"]
    
    p.join()
    

Je pense à en faire un module et à le mettre sur github, mais je dois d'abord nettoyer et commenter.

6
drunken_monkey