使用SQLAlchemy和多处理挂起Python脚本

时间:2012-01-09 09:00:49

标签: python postgresql sqlalchemy multiprocessing

考虑以下Python脚本,该脚本使用SQLAlchemy和Python多处理模块。 这是在Debian squeeze上使用Python 2.6.6-8 + b1(默认)和SQLAlchemy 0.6.3-3(默认)。 这是一些实际代码的简化版本。

import multiprocessing
from sqlalchemy import *
from sqlalchemy.orm import *
dbuser = ...
password = ...
dbname = ...
dbstring = "postgresql://%s:%s@localhost:5432/%s"%(dbuser, password, dbname)
db = create_engine(dbstring)
m = MetaData(db)

def make_foo(i):
    t1 = Table('foo%s'%i, m, Column('a', Integer, primary_key=True))

conn = db.connect()
for i in range(10):
    conn.execute("DROP TABLE IF EXISTS foo%s"%i)
conn.close()
db.dispose()

for i in range(10):
    make_foo(i)

m.create_all()

def do(kwargs):
    i, dbstring = kwargs['i'], kwargs['dbstring']

    db = create_engine(dbstring)
    Session = scoped_session(sessionmaker())
    Session.configure(bind=db)
    Session.execute("COMMIT; BEGIN; TRUNCATE foo%s; COMMIT;")
    Session.commit()
    db.dispose()

pool = multiprocessing.Pool(processes=5)               # start 4 worker processes
results = []
arglist = []
for i in range(10):
    arglist.append({'i':i, 'dbstring':dbstring})
r = pool.map_async(do, arglist, callback=results.append) # evaluate "f(10)" asynchronously
r.get()
r.wait()
pool.close()
pool.join()

此脚本挂起,并显示以下错误消息。

Exception in thread Thread-2:
Traceback (most recent call last):
  File "/usr/lib/python2.6/threading.py", line 532, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.6/threading.py", line 484, in run
    self.__target(*self.__args, **self.__kwargs)
  File "/usr/lib/python2.6/multiprocessing/pool.py", line 259, in _handle_results
    task = get()
TypeError: ('__init__() takes at least 4 arguments (2 given)', <class 'sqlalchemy.exc.ProgrammingError'>, ('(ProgrammingError) syntax error at or near "%"\nLINE 1: COMMIT; BEGIN; TRUNCATE foo%s; COMMIT;\n        ^\n',))

当然,这里的语法错误是TRUNCATE foo%s;。我的问题是,为什么这个过程悬而未决,我是否可以说服它以错误退出,而不对我的代码进行大手术?这种行为与我的实际代码非常相似。

请注意,如果语句被print foobarbaz替换,则不会发生挂起。此外,如果我们更换

,仍然会发生挂起
Session.execute("COMMIT; BEGIN; TRUNCATE foo%s; COMMIT;")
Session.commit()
db.dispose()

Session.execute("TRUNCATE foo%s;")

我正在使用以前的版本,因为它更接近我的实际代码。

此外,从图片中删除multiprocessing并循环遍历表格会导致挂起消失,并且只是退出时出现错误。

我也对错误的形式感到困惑,尤其是TypeError: ('__init__() takes at least 4 arguments (2 given)'位。这个错误来自哪里?它似乎可能来自multiprocessing代码中的某个地方。

PostgreSQL日志没有帮助。我看到很多像

这样的线条
2012-01-09 14:16:34.174 IST [7810] 4f0aa96a.1e82/1 12/583 0 ERROR:  syntax error at or near "%" at character 28
2012-01-09 14:16:34.175 IST [7810] 4f0aa96a.1e82/2 12/583 0 STATEMENT:  COMMIT; BEGIN; TRUNCATE foo%s; COMMIT;

但没有其他似乎相关的内容。

更新1:感谢lbolla和他的insightful analysis,我能够就此提出Python bug report。 请参阅该报告中的sbt分析,以及here。另请参阅Python错误报告Fix exception pickling。所以,按照sbt的解释,我们可以用

重现原始错误
import sqlalchemy.exc
e = sqlalchemy.exc.ProgrammingError("", {}, None)
type(e)(*e.args)

给出了

Traceback (most recent call last):
  File "<stdin>", line 9, in <module>
TypeError: __init__() takes at least 4 arguments (2 given)

更新2:至少对于SQLAlchemy,Mike Bayer已修复此问题,请参阅错误报告StatementError Exceptions un-pickable.。根据Mike的建议,我也向psycopg2报告了一个类似的错误,尽管我没有(并且没有)有一个破损的实际例子。无论如何,他们显然已经修复了它,尽管他们没有提供修复的细节。见psycopg exceptions cannot be pickled。为了更好地衡量,我还报告了与ConfigParser exceptions are not pickleable对应的Python错误the SO question lbolla mentioned。他们似乎想要对此进行测试。

无论如何,这看起来在可预见的未来仍会是一个问题,因为总的来说,Python开发人员似乎并没有意识到这个问题,所以不要防范它。令人惊讶的是,似乎没有足够的人使用多处理这是一个众所周知的问题,或者他们可能只是忍受它。我希望Python开发人员能够至少为Python 3修复它,因为它很烦人。

我接受了lbolla的回答,因为如果不解释问题与异常处理的关系,我可能无法理解这一点。我还要感谢sbt,他解释说Python无法解决异常问题。我非常感谢他们两个,请将他们的答案投票。感谢。

更新3:我发布了一个后续问题:Catching unpickleable exceptions and re-raising

4 个答案:

答案 0 :(得分:11)

我相信TypeError来自multiprocessing的{​​{1}}。

我已从您的脚本中删除了所有数据库代码。看看这个:

get

使用import multiprocessing import sqlalchemy.exc def do(kwargs): i = kwargs['i'] print i raise sqlalchemy.exc.ProgrammingError("", {}, None) return i pool = multiprocessing.Pool(processes=5) # start 4 worker processes results = [] arglist = [] for i in range(10): arglist.append({'i':i}) r = pool.map_async(do, arglist, callback=results.append) # evaluate "f(10)" asynchronously # Use get or wait? # r.get() r.wait() pool.close() pool.join() print results 会返回预期的结果,但使用r.wait会引发r.get。如python's docs中所述,请在TypeError之后使用r.wait

修改:我必须修改之前的回答。我现在相信map_async来自SQLAlchemy。我修改了我的脚本以重现错误。

编辑2 :看起来问题是TypeError如果任何工作人员引发构造函数需要参数的异常(参见here),则效果不佳。

我修改了我的脚本以突出显示这一点。

multiprocessing.pool

在您的情况下,假设您的代码引发了SQLAlchemy异常,我能想到的唯一解决方案是捕获import multiprocessing class BadExc(Exception): def __init__(self, a): '''Non-optional param in the constructor.''' self.a = a class GoodExc(Exception): def __init__(self, a=None): '''Optional param in the constructor.''' self.a = a def do(kwargs): i = kwargs['i'] print i raise BadExc('a') # raise GoodExc('a') return i pool = multiprocessing.Pool(processes=5) results = [] arglist = [] for i in range(10): arglist.append({'i':i}) r = pool.map_async(do, arglist, callback=results.append) try: # set a timeout in order to be able to catch C-c r.get(1e100) except KeyboardInterrupt: pass print results 函数中的所有异常并重新引发正常do。像这样:

Exception

编辑3 :所以,它似乎是bug with Python,但SQLAlchemy中的正确异常会解决它:因此,我也raised the issue with SQLAlchemy

作为解决问题的方法,我认为编辑2 结束时的解决方案会做(在try-except和re-raise中包装回调)。

答案 1 :(得分:2)

TypeError: ('__init__() takes at least 4 arguments (2 given)错误与您尝试执行的sql无关,它与您使用SqlAlchemy API的方式有关。

问题是你试图在会话类上调用execute而不是该会话的实例。

试试这个:

session = Session()
session.execute("COMMIT; BEGIN; TRUNCATE foo%s; COMMIT;")
session.commit()

来自the docs

  

打算在session中调用sessionmaker()函数   应用程序的全局范围,以及返回的类   作为单个类使用,可用于应用程序的其余部分   实例化会话。

所以Session = sessionmaker()返回一个新的会话类,session = Session()返回该类的一个实例,然后你可以调用execute

答案 2 :(得分:1)

我不知道原始异常的原因。但是,多处理“坏”异常的问题实际上取决于酸洗的工作原理。我认为sqlachemy异常类已经坏了。

如果异常类的__init__()方法没有直接或间接地调用BaseException.__init__(),则self.args可能无法正确设置。 BaseException.__reduce__()(由pickle协议使用)假定只需执行

即可重新创建异常e的副本
type(e)(*e.args)

例如

>>> e = ValueError("bad value")
>>> e
ValueError('bad value',)
>>> type(e)(*e.args)
ValueError('bad value',)

如果此不变量不成立,那么酸洗/去除将失败。所以

的实例
class BadExc(Exception):
    def __init__(self, a):
        '''Non-optional param in the constructor.'''
        self.a = a

可以被腌制,但结果不能被打开:

>>> from cPickle import loads, dumps
>>> class BadExc(Exception):
...     def __init__(self, a):
...         '''Non-optional param in the constructor.'''
...         self.a = a
...
>>> loads(dumps(BadExc(1)))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ('__init__() takes exactly 2 arguments (1 given)', <class '__main__.BadExc'>, ())

的实例
class GoodExc1(Exception):
    def __init__(self, a):
        '''Non-optional param in the constructor.'''
        Exception.__init__(self, a)
        self.a = a

class GoodExc2(Exception):
    def __init__(self, a):
        '''Non-optional param in the constructor.'''
        self.args = (a,)
        self.a = a

可以成功腌制/去除。

所以你应该让sqlalchemy的开发人员修复他们的异常类。与此同时,您可以使用copy_reg.pickle()覆盖BaseException.__reduce__()来处理麻烦的类。

答案 3 :(得分:1)

(这是对Faheem Mitha关于如何使用copy_reg来解决破坏的异常类的评论的回答。)

SQLAlchemy的异常类的__init__()方法似乎调用了它们的基类的__init__()方法,但是使用了不同的参数。这会弄脏酸洗。

要自定义sqlalchemy异常类的pickle,您可以使用copy_reg为这些类注册自己的reduce函数。

reduce函数接受参数obj并返回一对(callable_obj, args),以便通过obj创建callable_obj(*args)的副本。例如

class StatementError(SQLAlchemyError):
    def __init__(self, message, statement, params, orig):
        SQLAlchemyError.__init__(self, message)
        self.statement = statement
        self.params = params
        self.orig = orig
    ...

可以通过

“修复”
import copy_reg, sqlalchemy.exc

def reduce_StatementError(e):
    message = e.args[0]
    args = (message, e.statement, e.params, e.orig)
    return (type(e), args)

copy_reg.pickle(sqlalchemy.exc.StatementError, reduce_StatementError)

sqlalchemy.exc中还有其他几个类需要同样修复。但希望你明白了。


第二个想法,不是单独修复每个类,你可能只是修补基本异常类的__reduce__()方法:

import sqlalchemy.exc

def rebuild_exc(cls, args, dic):
    e = Exception.__new__(cls)
    e.args = args
    e.__dict__.update(dic)
    return e

def __reduce__(e):
    return (rebuild_exc, (type(e), e.args, e.__dict__))

sqlalchemy.exc.SQLAlchemyError.__reduce__ = __reduce__