考虑以下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。
答案 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__