You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
"Another operation is in progress" error when using transactions and asyncio.gather #313
Hello!
When i need to make several operations in parallel with db i'm using asyncio.gather with task and transaction inside it, but i have found that in GINO it does not work correctly - throws an error asyncpg.exceptions._base.InterfaceError: cannot perform operation: another operation is in progress
If i will place db.acquire before db.transaction it does not change anything.
I think that problem is that there must be new real connection and transaction in it when using asyncio.gather
stacktrace
await Model.query.gino.first()
File "venv/project/lib/python3.7/site-packages/gino/api.py", line 135, in first
**params)
File "venv/project/lib/python3.7/site-packages/gino/engine.py", line 681, in first
return await conn.first(clause, *multiparams, **params)
File "venv/project/lib/python3.7/site-packages/gino/engine.py", line 336, in first
return await result.execute(one=True)
File "venv/project/lib/python3.7/site-packages/gino/dialects/base.py", line 207, in execute
context.statement, context.timeout, args, 1 if one else 0)
File "venv/project/lib/python3.7/site-packages/gino/dialects/asyncpg.py", line 175, in async_execute
query, executor, timeout)
File "venv/project/lib/python3.7/site-packages/asyncpg/connection.py", line 1400, in _do_execute
stmt = await self._get_statement(query, None)
File "venv/project/lib/python3.7/site-packages/asyncpg/connection.py", line 323, in _get_statement
statement = await self._protocol.prepare(stmt_name, query, timeout)
File "asyncpg/protocol/protocol.pyx", line 165, in prepare
File "asyncpg/protocol/protocol.pyx", line 675, in asyncpg.protocol.protocol.BaseProtocol._check_state
asyncpg.exceptions._base.InterfaceError: cannot perform operation: another operation is in progress
i have added print(cursor._conn, cursor._conn.gino_conn, cursor._conn.raw_connection)
into /gino/dialects/base.py:206
there are differect connections as expected
<gino.engine._DBAPIConnection object at 0x7fcdc69b2080> <gino.ext.quart.GinoConnection object at 0x7fcdc69b2fd0> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7fcdc69b3048> 0x7fcdc3b590b8>
<gino.engine._DBAPIConnection object at 0x7fcdc3b591d0> <gino.ext.quart.GinoConnection object at 0x7fcdc3b59278> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7fcdc69b3120> 0x7fcdc3b592b0>
<gino.engine._DBAPIConnection object at 0x7fcdc3b59438> <gino.ext.quart.GinoConnection object at 0x7fcdc3b594e0> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7fcdc69b32d0> 0x7fcdc3b59518>
<gino.engine._DBAPIConnection object at 0x7fcdc3b596a0> <gino.ext.quart.GinoConnection object at 0x7fcdc3b59748> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7fcdc69b31f8> 0x7fcdc3b59780>
and without bind=tx.connection result is following
only one connection is used and error is raised like in the first mesage
<gino.engine._ReusingDBAPIConnection object at 0x7ff45c77cd30> <gino.ext.quart.GinoConnection object at 0x7ff45c77ce10> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7ff45c821480> 0x7ff45c77ce48>
<gino.engine._ReusingDBAPIConnection object at 0x7ff45c77cef0> <gino.ext.quart.GinoConnection object at 0x7ff45c77cfd0> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7ff45c821480> 0x7ff45c77ce48>
<gino.engine._ReusingDBAPIConnection object at 0x7ff45c7b3080> <gino.ext.quart.GinoConnection object at 0x7ff45c7b3160> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7ff45c821480> 0x7ff45c77ce48>
<gino.engine._ReusingDBAPIConnection object at 0x7ff45c7b31d0> <gino.ext.quart.GinoConnection object at 0x7ff45c7b32b0> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7ff45c821480> 0x7ff45c77ce48>
<gino.engine._ReusingDBAPIConnection object at 0x7ff45c7b33c8> <gino.ext.quart.GinoConnection object at 0x7ff45c7b3518> <PoolConnectionProxy <asyncpg.connection.Connection object at 0x7ff45c821480> 0x7ff45c77ce48>
Traceback (most recent call last):
File "/venv/project/lib/python3.6/site-packages/quart/app.py", line 1419, in handle_request
return await self.full_dispatch_request(request_context)
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 129, in throw
return self.gen.throw(type, value, traceback)
File "/venv/project/lib/python3.6/site-packages/quart/app.py", line 1441, in full_dispatch_request
result = await self.handle_user_exception(error)
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/quart/flask_patch/app.py", line 23, in new_handle_user_exception
return await old_handle_user_exception(self, error)
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/quart/app.py", line 876, in handle_user_exception
raise error
File "/venv/project/lib/python3.6/site-packages/quart/app.py", line 1439, in full_dispatch_request
result = await self.dispatch_request(request_context)
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 129, in throw
return self.gen.throw(type, value, traceback)
File "/venv/project/lib/python3.6/site-packages/quart/app.py", line 1487, in dispatch_request
return await handler(**request_.view_args)
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 129, in throw
return self.gen.throw(type, value, traceback)
File "/project/views.py", line 594, in test_save
await asyncio.gather(*[t() for i in range(5)])
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 126, in send
return self.gen.send(value)
File "/project/views.py", line 591, in t
async with db.transaction():
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/gino/engine.py", line 168, in __aenter__
return await self._tx_ctx.__aenter__()
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/gino/transaction.py", line 165, in __aenter__
await self._begin()
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/gino/transaction.py", line 80, in _begin
await self._tx.begin()
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/gino/dialects/asyncpg.py", line 238, in begin
await self._tx.start()
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/asyncpg/transaction.py", line 138, in start
await self._connection.execute(query)
File "/.pyenv/versions/3.6.6/lib/python3.6/asyncio/coroutines.py", line 110, in __next__
return self.gen.send(None)
File "/venv/project/lib/python3.6/site-packages/asyncpg/connection.py", line 273, in execute
return await self._protocol.query(query, timeout)
File "asyncpg/protocol/protocol.pyx", line 317, in query
File "asyncpg/protocol/protocol.pyx", line 675, in asyncpg.protocol.protocol.BaseProtocol._check_state
asyncpg.exceptions._base.InterfaceError: cannot perform operation: another operation is in progress
Ah yes, thanks for the update! To reproduce this issue in Python 3.6, aiocontextvars.enable_inherit() is required, which is the default for web framework GINO plugins.
async with db.transaction():
File "/venv//lib/python3.7/site-packages/gino/engine.py", line 168, in __aenter__
return await self._tx_ctx.__aenter__()
File "/venv//lib/python3.7/site-packages/gino/transaction.py", line 166, in __aenter__
await self._begin()
File "/venv//lib/python3.7/site-packages/gino/transaction.py", line 81, in _begin
await self._tx.begin()
File "/venv//lib/python3.7/site-packages/gino/dialects/asyncpg.py", line 238, in begin
await self._tx.start()
File "/venv//lib/python3.7/site-packages/asyncpg/transaction.py", line 138, in start
await self._connection.execute(query)
File "/venv//lib/python3.7/site-packages/asyncpg/connection.py", line 273, in execute
return await self._protocol.query(query, timeout)
File "asyncpg/protocol/protocol.pyx", line 317, in query
File "asyncpg/protocol/protocol.pyx", line 675, in asyncpg.protocol.protocol.BaseProtocol._check_state
asyncpg.exceptions._base.InterfaceError: cannot perform operation: another operation is in progress
The issue is with db.acquire() happening before subtasks start, they share the common context stack, in which case the subsequent tasks get the same connection pushed by the first task.
I don't have a clear answer in mind if gino should create a new connection, or have a lock-like mechanism on the connection. I assume there are valid use cases in both scenarios.
@wwwjfy My idea is to remove stack instances from shared contexts once stacks became empty. This requires unique remove APIs for Python 3.7 and aiocontextvars or preferably contextvars backport (MagicStack/contextvars#11).
@fantix That can fix the problem raised, but can't prevent that the same reused connection got by subtasks, like the snippet in my previous comment. Because scalar has self.acquire(reuse=True), if the connection is being used by another subtask, the exception will be thrown.
Ah, first of all, context should be inherited in subtasks, this was discussed in #84, PEP-550:
The main reason for why tasks inherit the context, and threads do not, is the common usage intent. Tasks are often used for relatively short-running operations which are logically tied to the code that spawned the task (like running a coroutine with a timeout in asyncio). OS threads, on the other hand, are normally used for long-running, logically separate code.
Tasks in asyncio need to maintain their own context that they inherit from the point they were created at.
Regarding shared connections, I think it should follow the way contexts are inherited, so that people don't have to remember another rule. Therefore if users are spawning subtasks within an async with db.acquire() context, they should be responsible for causing such error.
In your example, if user was in doubt whether the reusing connection may be accessed concurrently, an explicit reuse=False should always be present. But in case of this:
I have no issues on context inheritance, but not sure if there is any case user wants to use only one connection for those subtasks or in one request, where we'll need to lock-release it.
I just found that the fixes for this (the upcoming 0.8.0 with aiocontextvars 0.2?) likely also fix my woes with transactions.
I'm explicitly not using the aiohttp integration because the web part is just one side of my application, that also reacts to external events, so I didn't have the enable_inherit call anywhere. Long story short, whenever I used asyncio.gather to fork execution from within a transaction context, the current connection wouldn't be reused, so each of the coroutines I ran gather on used their own, transaction-less one, and I got plenty of "race conditions" that I was counting on the serializable isolation level to prevent.
I got to this by looking at PostgreSQL's statement logs where I couldn't see savepoints that I expected nested transactions to trigger. I dug all the code up to asyncpg's transaction support to verify they should be there. Eventually realized by the PIDs in the logs that different connections were in use when there should be only one.
I've just checked against master head, and now I see shared connections and nested savepoints, so I expect I'll see serialization failure retries now :-)
BTW. This is running on Python 3.5.3, and I made sure to have aiocontextvars imported before creating any loops.
Glad you find it works. One thing I'm thinking is using asyncio.gather, if it's used in your issue description, different coroutines share the same connection. This may cause nested transactions conflict with each other.
I don't use nested transactions much. This may not be true. I'll try it later.
@wwwjfy you're right; I had not thought of different nested transactions being started in different branches. That's not going to work.
But I don't need them. I just need to know if a transaction is already active (and let it retry as a whole if it fails) or not (and create a new root one in the spot).
I'll likely upgrade my decorator to use aiocontextvars to implement that behavior rather than blindly starting a new (sub) transaction. That will also allow me to register actions to be run once a transaction has been irreversibly committed.
But losing the current connection just because I used asyncio.gather was completely unexpected.
Activity
jekel commentedon Aug 21, 2018
So.. i have made investigation and found that without implicit connection passing all nested quieries are made not in connection of db.transaction :(
when code is
i have added
print(cursor._conn, cursor._conn.gino_conn, cursor._conn.raw_connection)into /gino/dialects/base.py:206
there are differect connections as expected
and without
bind=tx.connectionresult is followingonly one connection is used and error is raised like in the first mesage
fantix commentedon Aug 22, 2018
Thanks for the report! I'm seeing two critical issues for GINO in Python 3.7:
db.acquire(reuse=False)won't fail, butdb.transaction(reuse=False)will.jekel commentedon Aug 22, 2018
This error also is valid for python 3.6.
i've tried as initialization
with different parameters combination also
But it does not work also:
Is there anyway to use
db.acquire(reuse=False)in all nested queries inside context block without passing any variables explicitly?fantix commentedon Aug 24, 2018
Could you please share your env and errors when above code fails in Python 3.6? I didn't reproduce in Python 3.6.
FYI
db.acquire()hasreuse=Falseby default.jekel commentedon Aug 24, 2018
Python 3.6.6 (default, Aug 20 2018, 16:26:45)
fantix commentedon Aug 28, 2018
Ah yes, thanks for the update! To reproduce this issue in Python 3.6,
aiocontextvars.enable_inherit()is required, which is the default for web framework GINO plugins.jekel commentedon Aug 29, 2018
@fantix You know, the same issue is also valid for Sanic
fantix commentedon Aug 29, 2018
Yes thanks, all web framework plugins in GINO enabled inherit. I'm trying to make that a default in upstream contextvars PEP-567 backport.
wwwjfy commentedon Sep 2, 2018
It doesn't matter whether the connection was acquired and released in the main task. It also happens in the following code:
The issue is with
db.acquire()happening before subtasks start, they share the common contextstack, in which case the subsequent tasks get the same connection pushed by the first task.I don't have a clear answer in mind if gino should create a new connection, or have a lock-like mechanism on the connection. I assume there are valid use cases in both scenarios.
fantix commentedon Sep 4, 2018
@wwwjfy My idea is to remove stack instances from shared contexts once stacks became empty. This requires unique remove APIs for Python 3.7 and
aiocontextvarsor preferablycontextvarsbackport (MagicStack/contextvars#11).wwwjfy commentedon Sep 4, 2018
@fantix That can fix the problem raised, but can't prevent that the same reused connection got by subtasks, like the snippet in my previous comment. Because
scalarhasself.acquire(reuse=True), if the connection is being used by another subtask, the exception will be thrown.fantix commentedon Sep 4, 2018
Ah, first of all, context should be inherited in subtasks, this was discussed in #84, PEP-550:
and eventually implemented in PEP-567:
Regarding shared connections, I think it should follow the way contexts are inherited, so that people don't have to remember another rule. Therefore if users are spawning subtasks within an
async with db.acquire()context, they should be responsible for causing such error.In your example, if user was in doubt whether the reusing connection may be accessed concurrently, an explicit
reuse=Falseshould always be present. But in case of this:db.status()should reuse the connection explicitly acquired previously, even ifasyncio.wait_for()puts it into a subtask.Hopefully this explains. 😃
wwwjfy commentedon Sep 4, 2018
I have no issues on context inheritance, but not sure if there is any case user wants to use only one connection for those subtasks or in one request, where we'll need to lock-release it.
fantix commentedon Sep 4, 2018
Ah, got it. I think the users should lock-release it themselves manually, likewise, asyncpg didn't provide locking either.
wwwjfy commentedon Sep 4, 2018
ok, fair enough.
Removing
stackfrom context shouldn't be very hard.fantix commentedon Sep 4, 2018
No it's not - I'm trying to remove
aiocontextvars.ContextVar.delete()and honorcontextvars.ContextVar.reset(token).Fixed #313, remove stack when empty
Merge pull request #334 from fantix/t313
fantix commentedon Sep 30, 2018
Guess this should be backported to GINO 0.7 without introducing aiocontextvars 0.2.
skandalfo commentedon Oct 7, 2018
Wow.
I just found that the fixes for this (the upcoming 0.8.0 with aiocontextvars 0.2?) likely also fix my woes with transactions.
I'm explicitly not using the aiohttp integration because the web part is just one side of my application, that also reacts to external events, so I didn't have the enable_inherit call anywhere. Long story short, whenever I used asyncio.gather to fork execution from within a transaction context, the current connection wouldn't be reused, so each of the coroutines I ran gather on used their own, transaction-less one, and I got plenty of "race conditions" that I was counting on the serializable isolation level to prevent.
I got to this by looking at PostgreSQL's statement logs where I couldn't see savepoints that I expected nested transactions to trigger. I dug all the code up to asyncpg's transaction support to verify they should be there. Eventually realized by the PIDs in the logs that different connections were in use when there should be only one.
I've just checked against master head, and now I see shared connections and nested savepoints, so I expect I'll see serialization failure retries now :-)
BTW. This is running on Python 3.5.3, and I made sure to have aiocontextvars imported before creating any loops.
wwwjfy commentedon Oct 8, 2018
Glad you find it works. One thing I'm thinking is using
asyncio.gather, if it's used in your issue description, different coroutines share the same connection. This may cause nested transactions conflict with each other.I don't use nested transactions much. This may not be true. I'll try it later.
skandalfo commentedon Oct 8, 2018
@wwwjfy you're right; I had not thought of different nested transactions being started in different branches. That's not going to work.
But I don't need them. I just need to know if a transaction is already active (and let it retry as a whole if it fails) or not (and create a new root one in the spot).
I'll likely upgrade my decorator to use aiocontextvars to implement that behavior rather than blindly starting a new (sub) transaction. That will also allow me to register actions to be run once a transaction has been irreversibly committed.
But losing the current connection just because I used asyncio.gather was completely unexpected.