]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Avoid creating asyncio.Lock on the wrong loop.
authorFederico Caselli <cfederico87@gmail.com>
Fri, 30 Apr 2021 20:36:59 +0000 (22:36 +0200)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 1 May 2021 15:58:13 +0000 (11:58 -0400)
Fixed a regression introduced by :ticket:`6337` that would create an
``asyncio.Lock`` which could be attached to the wrong loop when
instantiating the async engine before any asyncio loop was started, leading
to an asyncio error message when attempting to use the engine under certain
circumstances.

Fixes: #6409
Change-Id: I8119c56b44a7bd70a650c0ea676892d4d7814a8b

doc/build/changelog/unreleased_14/6409.rst [new file with mode: 0644]
lib/sqlalchemy/util/_concurrency_py3k.py
test/ext/asyncio/test_engine_py3k.py

diff --git a/doc/build/changelog/unreleased_14/6409.rst b/doc/build/changelog/unreleased_14/6409.rst
new file mode 100644 (file)
index 0000000..a5b1da2
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, asyncio, regression
+    :tickets: 6409
+
+    Fixed a regression introduced by :ticket:`6337` that would create an
+    ``asyncio.Lock`` which could be attached to the wrong loop when
+    instantiating the async engine before any asyncio loop was started, leading
+    to an asyncio error message when attempting to use the engine under certain
+    circumstances.
index b905f903b83fec6199db8ab84db7e27ae589a3ac..5f03972b2a4ab1fc21049a71a5986786809583e7 100644 (file)
@@ -7,6 +7,7 @@ from typing import Coroutine
 import greenlet
 
 from . import compat
+from .langhelpers import memoized_property
 from .. import exc
 
 
@@ -132,10 +133,15 @@ async def greenlet_spawn(
 
 
 class AsyncAdaptedLock:
-    def __init__(self):
-        self.mutex = asyncio.Lock()
+    @memoized_property
+    def mutex(self):
+        # there should not be a race here for coroutines creating the
+        # new lock as we are not using await, so therefore no concurrency
+        return asyncio.Lock()
 
     def __enter__(self):
+        # await is used to acquire the lock only after the first calling
+        # coroutine has created the mutex.
         await_fallback(self.mutex.acquire())
         return self
 
index dc791215cf2d1371c933b7f657ec721565155db2..820c82bca6d1ff7d016e6f5822a7bb35a04a0e69 100644 (file)
@@ -90,6 +90,43 @@ class AsyncEngineTest(EngineFixture):
 
         is_false(async_engine == None)
 
+    @async_test
+    async def test_no_attach_to_event_loop(self, testing_engine):
+        """test #6409"""
+
+        import asyncio
+        import threading
+
+        errs = []
+
+        def go():
+            loop = asyncio.new_event_loop()
+            asyncio.set_event_loop(loop)
+
+            engine = testing_engine(asyncio=True, transfer_staticpool=True)
+
+            async def main():
+                tasks = [task() for _ in range(2)]
+
+                await asyncio.gather(*tasks)
+
+            async def task():
+                async with engine.begin() as connection:
+                    result = await connection.execute(select(1))
+                    result.all()
+
+            try:
+                asyncio.run(main())
+            except Exception as err:
+                errs.append(err)
+
+        t = threading.Thread(target=go)
+        t.start()
+        t.join()
+
+        if errs:
+            raise errs[0]
+
     @async_test
     async def test_connection_info(self, async_engine):