On Thursday, 9 January 2014 09:41:40 UTC-8, Jeff Dairiki wrote: > > Okay, I've traced things out a bit more. > > If the session state is not STATUS_INVALIDATED (aka STATUS_CHANGED), > SessionDataManager.commit() does a self._finish('no work'). That is > where self.tx gets set to None (this --- correctly --- then > causes .tpc_vote() and .tpc_finish() to be no-ops.) > > So here's the crux of the biscuit: in two-phase-commit mode (at least > with MySQL) the sqlalchemy session (or session transaction) must be either > committed or explicitly rolled back before it is closed. > SessionDataManager.commit() does not do a rollback. > > Example code: > > import sqlalchemy as sa > > engine = sa.create_engine('mysql://guest@furry/test', > echo='debug', > echo_pool='debug') > Sess = sa.orm.sessionmaker(bind=engine, twophase=True) > sess = Sess() > sess.query(sa.null()).scalar() > #sess.rollback() > sess.close() > > Edited log output: > > DEBUG:sqlalchemy.pool.QueuePool:Connection <_mysql.connection open to > 'furry' at 29a3370> checked out from pool > INFO:sqlalchemy.engine.base.Engine:BEGIN TWOPHASE (implicit) > INFO:sqlalchemy.engine.base.Engine:XA BEGIN %s > > INFO:sqlalchemy.engine.base.Engine:('_sa_a38de3b7234d5fe8ad55d2bb13ec195c',) > > INFO:sqlalchemy.engine.base.Engine:SELECT NULL AS anon_1 > DEBUG:sqlalchemy.pool.QueuePool:Connection <_mysql.connection open to > 'furry' at 29a3370> being returned to pool > DEBUG:sqlalchemy.pool.QueuePool:Connection <_mysql.connection open to > 'furry' at 29a3370> rollback-on-return > INFO:sqlalchemy.pool.QueuePool:Invalidate connection > <_mysql.connection open to 'furry' at 29a3370> (reason: > OperationalError:(1399, 'XAER_RMFAIL: The command cannot be executed when > global transaction is in the ACTIVE state')) > DEBUG:sqlalchemy.pool.QueuePool:Closing connection <_mysql.connection > open to 'furry' at 29a3370> > > Here, no attempt is made to terminate the two-phase (XA) transaction > until the connection is returned to the pool, at which point a > plain one-phase 'ROLLBACK' is issued. MySQL does not like this, > thus the XAER_RMFAIL error. > > Uncommenting the 'sess.rollback()' in the above example results in an > 'XA END" and 'XA ROLLBACK' being emitted before the connection is > returned to the pool, properly ending the two-phase (XA) transaction. > This eliminates the XAER_RMFAIL error, and results in proper recycling > of the pooled connection. > Yup, your analysis here looks correct to me:
- The SessionTransaction.close() does not call transaction.close() and in turn transaction.rollback() because the connection's autoclose is True as ``conn is not bind`` - https://github.com/zzzeek/sqlalchemy/blob/rel_0_9_1/lib/sqlalchemy/orm/session.py#L431 https://github.com/zzzeek/sqlalchemy/blob/rel_0_9_1/lib/sqlalchemy/orm/session.py#L297 When the connection is returned to the pool, it will rollback-on-return and that's throwing the exception as it is simply calling ``pool._dialect.do_rollback(fairy)`` - https://github.com/zzzeek/sqlalchemy/blob/rel_0_9_1/lib/sqlalchemy/pool.py#L408 When you call session.rollback() the engine's ``do_rollback_twophase()`` is being called which executes the appropirate "XA ROLLBACK :xid" - https://github.com/zzzeek/sqlalchemy/blob/rel_0_9_1/lib/sqlalchemy/dialects/mysql/base.py#L2086 I think the ``pool._dialect.do_rollback`` and ``pool._dialect.do_commit`` calls in _finalize_fairy need to take account of the twophase nature of the connection. I don't think zope.sqlalchemy should be involved here, as the pool configures how a connection should be closed with reset_on_return: http://docs.sqlalchemy.org/en/rel_0_9/core/pooling.html#sqlalchemy.pool.QueuePool.__init__.params.reset_on_return Worth reporting as a SQLAlchemy bug, though fixing it cleanly looks rather tricky. You might be able to rig something together with event listeners, but it looks tricky. For the pools reset event you won't have access to the xid and is_prepared of the TwoPhaseTransaction, so you'll need to store these on the connection during: http://docs.sqlalchemy.org/en/rel_0_9/core/events.html#sqlalchemy.events.PoolEvents.reset http://docs.sqlalchemy.org/en/rel_0_9/core/events.html#sqlalchemy.events.ConnectionEvents.begin_twophase http://docs.sqlalchemy.org/en/rel_0_9/core/events.html#sqlalchemy.events.ConnectionEvents.prepare_twophase I think this should do it (untested): from sqlalchemy import event @event.listens_for(SomeEngine, 'begin_twophase') def receive_begin_twophase(conn, xid): conn._note_xid_prepared = (xid, False) @event.listens_for(SomeEngine, 'prepare_twophase') def receive_prepare_twophase(conn, xid): conn._note_xid_prepared = (xid, True) @event.listens_for(SomeEngineOrPool, 'reset') def receive_reset(dbapi_con, con_record): conn = con_record.connection if not hasattr(conn, '_note_xid_prepared'): return xid, is_prepared = conn._note_xid_prepared del conn._note_xid_prepared conn.dialect.do_rollback_twophase(xid, is_prepared) Hope that helps, Laurence -- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To unsubscribe from this group and stop receiving emails from it, send an email to sqlalchemy+unsubscr...@googlegroups.com. To post to this group, send email to sqlalchemy@googlegroups.com. Visit this group at http://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/groups/opt_out.