web-dev-qa-db-ja.com

Python Multiprocessing:親の子エラーの処理

現在、マルチプロセッシングとキューをいじっています。 mongoDBからデータをエクスポートし、リレーショナル(フラット)構造にマップし、すべての値を文字列に変換してmysqlに挿入するコードを記述しました。

これらの各ステップはプロセスとして送信され、インポート/エクスポートキューが与えられ、親で処理されるmongoDBエクスポートに対して安全です。

以下に示すように、私はキューを使用し、子プロセスはキューから「なし」を読み取ったときに自身を終了します。私が現在抱えている問題は、子プロセスが未処理の例外に遭遇した場合、これは親によって認識されず、残りは実行を続けることです。私がしたいのは、シバン全体が終了し、せいぜい子供のエラーをリレイズすることです。

2つの質問があります。

  1. 親の子エラーを検出するにはどうすればよいですか?
  2. エラーを検出した後、子プロセスを強制終了するにはどうすればよいですか(ベストプラクティス)。子供を殺すために「なし」をキューに入れることはかなり汚いことを理解しています。

私はpython 2.7を使用しています。

コードの重要な部分は次のとおりです。

# 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()

[... 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

私は標準的なプラクティスを知りませんが、私が見つけたのは、信頼できるマルチプロセッシングを行うために、メソッド/クラス/などを設計することです。特にマルチプロセッシングで動作します。そうしないと、反対側で何が起こっているのかを実際に知ることはできません(このための何らかのメカニズムを見逃していない限り)。

具体的に私がやることは:

  • サブクラスmultiprocessing.Processまたはマルチプロセッシングを特にサポートする関数を作成する(必要に応じて制御できない関数をラップする)
  • メインプロセスから各ワーカープロセスへの共有エラーmultiprocessing.Queueを常に提供する
  • 実行コード全体をtry: ... except Exception as eで囲みます。その後、予期しないことが発生した場合、次のエラーパッケージを送信します。
    • 死んだプロセスID
    • 元のコンテキストの例外( ここをチェック )。メインプロセスで有用な情報を記録する場合、元のコンテキストは非常に重要です。
  • もちろん、ワーカーの通常の操作内で予想される問題を通常どおり処理します。
  • (既に述べたことと同様に)長時間実行されるプロセスを想定して、実行中のコード(try/catch-all内)をループでラップします
    • クラスまたは関数でストップトークンを定義します。
    • メインプロセスがワーカーの停止を希望する場合は、停止トークンを送信するだけです。全員を停止するには、すべてのプロセスに十分な数を送信します。
    • ラッピングループは、トークンの入力qまたは必要な他の入力をチェックします。

最終結果は、長期間存続できるワーカープロセスであり、何か問題が発生したときに何が起こっているかを知らせることができます。キャッチオール例外の後、必要なことは何でも処理でき、ワーカーをいつ再起動する必要があるかもわかるので、それらは静かに死にます。

繰り返しますが、私は試行錯誤を繰り返してこのパターンにたどり着きました。それはあなたが求めているものに役立ちますか?

28
KobeJohn

次のように、プロセスに独自の例外を処理させない理由:

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

これで、エラーとトレースバックの両方を手に入れることができます。

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

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

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

よろしく、マレク

21
mrkwjc

Kobejohnのおかげで、ナイスで安定したソリューションを見つけました。

  1. いくつかの関数を実装し、run()メソッドを上書きして新しいsaferunメソッドをtry-catchブロックにラップするmultiprocessing.Processのサブクラスを作成しました。このクラスは、情報、デバッグ、エラーメッセージを親に報告するために使用される初期化するためにfeedback_queueを必要とします。クラス内のログメソッドは、パッケージのグローバルに定義されたログ関数のラッパーです。

    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. EtlStepProcessの他のすべてのプロセスステップをサブクラス化しました。実行されるコードは、実行ではなくsaferun()メソッドで実装されます。これにより、run()メソッドによって既に行われているため、try catchブロックを追加する必要がありません。例:

    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. メインファイルで、すべての作業を行うプロセスを送信し、それにfeedback_queueを付けます。このプロセスはすべてのステップを開始し、mongoDBから読み取り、値を初期キューに入れます。私のメインプロセスはフィードバックキューをリッスンし、すべてのログメッセージを出力します。エラーログを受信した場合、エラーを出力し、その子を終了します。これにより、死ぬ前にすべての子も終了します。

    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()
    

モジュールを作成してgithubに配置することを考えていますが、最初にいくつかのクリーンアップとコメントを行う必要があります。

6
drunken_monkey