MySQL服务器已断开 - 结账事件处理程序的断开处理无效
更新 3/4:
我做了一些测试,证明使用 checkout 事件处理程序来检查断开连接在 Elixir 中是有效的。 我开始觉得我的问题可能和从子进程调用 更新:我刚刚推翻了自己的想法,通过在子进程中调用 session.commit()
有关?session.commit()
,下面的例子已经更新。我正在使用 multiprocessing 模块来创建子进程。
下面是展示应该如何工作的代码(甚至不需要使用 pool_recycle
!):
from sqlalchemy import exc
from sqlalchemy import event
from sqlalchemy.pool import Pool
from elixir import *
import multiprocessing as mp
class SubProcess(mp.Process):
def run(self):
a3 = TestModel(name="monkey")
session.commit()
class TestModel(Entity):
name = Field(String(255))
@event.listens_for(Pool, "checkout")
def ping_connection(dbapi_connection, connection_record, connection_proxy):
cursor = dbapi_connection.cursor()
try:
cursor.execute("SELECT 1")
except:
# optional - dispose the whole pool
# instead of invalidating one at a time
# connection_proxy._pool.dispose()
# raise DisconnectionError - pool will try
# connecting again up to three times before raising.
raise exc.DisconnectionError()
cursor.close()
from sqlalchemy import create_engine
metadata.bind = create_engine("mysql://foo:bar@localhost/some_db", echo_pool=True)
setup_all(True)
subP = SubProcess()
a1 = TestModel(name='foo')
session.commit()
# pool size is now three.
print "Restart the server"
raw_input()
subP.start()
#a2 = TestModel(name='bar')
#session.commit()
更新 2:
由于 MySQL-python 1.2.2 版本之后不再支持 reconnect 参数,我被迫寻找其他解决方案。有没有人有解决办法? :\
更新 1(旧解决方案,不适用于 MySQL-python 版本 > 1.2.2):
找到了解决方案:将 connect_args={'reconnect':True}
传递给 create_engine
调用可以解决问题,自动重新连接。似乎甚至不需要 checkout 事件处理程序。
所以,在问题中的例子里:
metadata.bind = create_engine("mysql://foo:bar@localhost/db_name", pool_size=100, pool_recycle=3600, connect_args={'reconnect':True})
原始问题:
我在谷歌上搜索了很久这个问题,但似乎没有找到针对 Elixir 的具体解决方案——我试图使用 SQLAlchemy 文档中的 "断开连接处理 - 悲观" 示例来处理 MySQL 的断开连接。然而,当我测试这个(通过重启 MySQL 服务器)时,"MySQL server has gone away" 的错误在我的 checkout 事件处理程序之前就被抛出了。
这是我用来初始化 Elixir 的代码:
##### Initialize elixir/SQLAlchemy
# Disconnect handling
from sqlalchemy import exc
from sqlalchemy import event
from sqlalchemy.pool import Pool
@event.listens_for(Pool, "checkout")
def ping_connection(dbapi_connection, connection_record, connection_proxy):
logging.debug("***********ping_connection**************")
cursor = dbapi_connection.cursor()
try:
cursor.execute("SELECT 1")
except:
logging.debug("######## DISCONNECTION ERROR #########")
# optional - dispose the whole pool
# instead of invalidating one at a time
# connection_proxy._pool.dispose()
# raise DisconnectionError - pool will try
# connecting again up to three times before raising.
raise exc.DisconnectionError()
cursor.close()
metadata.bind= create_engine("mysql://foo:bar@localhost/db_name", pool_size=100, pool_recycle=3600)
setup_all()
我创建了 Elixir 实体对象,并通过 session.commit()
保存它们,在这个过程中我看到上面定义的事件生成的 "ping_connection" 消息。然而,当我重启 MySQL 服务器并再次测试时,它在 ping 连接事件之前就失败了,显示了 mysql server has gone away 的消息。
以下是从相关行开始的堆栈跟踪:
File "/usr/local/lib/python2.6/dist-packages/elixir/entity.py", line 1135, in get_by
return cls.query.filter_by(*args, **kwargs).first()
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 1963, in first
ret = list(self[0:1])
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 1857, in __getitem__
return list(res)
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 2032, in __iter__
return self._execute_and_instances(context)
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 2047, in _execute_and_instances
result = conn.execute(querycontext.statement, self._params)
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1399, in execute
params)
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1532, in _execute_clauseelement
compiled_sql, distilled_params
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1640, in _execute_context
context)
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1633, in _execute_context
context)
File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/default.py", line 330, in do_execute
cursor.execute(statement, parameters)
File "/usr/lib/pymodules/python2.6/MySQLdb/cursors.py", line 166, in execute
self.errorhandler(self, exc, value)
File "/usr/lib/pymodules/python2.6/MySQLdb/connections.py", line 35, in defaulterrorhandler
raise errorclass, errorvalue
OperationalError: (OperationalError) (2006, 'MySQL server has gone away')
4 个回答
你是在同一个会话中进行操作吗?也就是说,在重启mysqld之前和之后都是用的同一个会话?如果是这样的话,"checkout"
事件只会在你开始一个新事务的时候发生。当你调用commit()
时,就会开始一个新的事务(除非你使用的是自动提交模式),这时连接就会被“签出”。所以你是在“签出”之后重启mysqld的。
在第二次操作之前(并且在重启mysqld之后)简单地调用一下commit()
或rollback()
,应该能解决你的问题。否则的话,考虑每次在上一次提交后等了很久时,使用一个新的会话。
实际的问题是,每次你调用sessionmaker工厂的时候,sqlalchemy都会给你同一个会话。这就可能导致你在后面查询的时候,使用的是一个很早之前打开的会话,只要你没有调用session.remove()
。每次请求会话的时候都要记得调用remove()
可真让人头疼,不过sqlalchemy提供了一个更简单的解决方案:上下文“作用域”会话。
要创建一个作用域会话,只需把你的sessionmaker包裹起来:
from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker())
这样每次你调用工厂的时候,都会得到一个上下文绑定的会话,这意味着一旦调用的函数结束,sqlalchemy会自动为你调用session.remove()
。详细信息可以查看这里:sqlalchemy - 上下文会话的生命周期
最后的解决办法是在处理和加载elixir实体之前,先调用session.remove()
。这样做的效果是把连接归还给连接池,这样下次再用的时候,连接池会触发一个事件,我们的处理程序就能检测到连接断开了。来自SQLAlchemy文档:
在请求结束时并不一定要移除会话,还有其他选择,比如在结束时调用Session.close()、Session.rollback()或Session.commit(),这样现有的会话就会把连接归还给连接池,并清除任何现有的事务上下文。如果不做任何操作也是可以的,只要每个控制器方法负责确保在请求结束后没有事务保持打开状态。
这是个很重要的小信息,我希望在elixir文档中能提到。不过我想这可能假设了读者对SQLAlchemy有一定的了解吧?