]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
Fix waiter cancellation in asyncio.Lock (#1031)
authorMathieu Sornay <msornay@users.noreply.github.com>
Fri, 9 Jun 2017 20:17:40 +0000 (22:17 +0200)
committerYury Selivanov <yury@magic.io>
Fri, 9 Jun 2017 20:17:40 +0000 (16:17 -0400)
Avoid a deadlock when the waiter who is about to take the lock is
cancelled

Issue #27585

Lib/asyncio/locks.py
Lib/test/test_asyncio/test_locks.py

index deefc938ecfb01d3c0c7438dc6690d42711ffc71..92661830a0622830da008527b6fca71314b35d36 100644 (file)
@@ -176,6 +176,10 @@ class Lock(_ContextManagerMixin):
             yield from fut
             self._locked = True
             return True
+        except futures.CancelledError:
+            if not self._locked:
+                self._wake_up_first()
+            raise
         finally:
             self._waiters.remove(fut)
 
@@ -192,14 +196,17 @@ class Lock(_ContextManagerMixin):
         """
         if self._locked:
             self._locked = False
-            # Wake up the first waiter who isn't cancelled.
-            for fut in self._waiters:
-                if not fut.done():
-                    fut.set_result(True)
-                    break
+            self._wake_up_first()
         else:
             raise RuntimeError('Lock is not acquired.')
 
+    def _wake_up_first(self):
+        """Wake up the first waiter who isn't cancelled."""
+        for fut in self._waiters:
+            if not fut.done():
+                fut.set_result(True)
+                break
+
 
 class Event:
     """Asynchronous equivalent to threading.Event.
index 152948c8138975096aaa243005e95f155f551d4b..c85e8b1a32f73a9ea460f084e692fcbf0d376b72 100644 (file)
@@ -176,6 +176,28 @@ class LockTests(test_utils.TestCase):
         self.assertTrue(tb.cancelled())
         self.assertTrue(tc.done())
 
+    def test_finished_waiter_cancelled(self):
+        lock = asyncio.Lock(loop=self.loop)
+
+        ta = asyncio.Task(lock.acquire(), loop=self.loop)
+        test_utils.run_briefly(self.loop)
+        self.assertTrue(lock.locked())
+
+        tb = asyncio.Task(lock.acquire(), loop=self.loop)
+        test_utils.run_briefly(self.loop)
+        self.assertEqual(len(lock._waiters), 1)
+
+        # Create a second waiter, wake up the first, and cancel it.
+        # Without the fix, the second was not woken up.
+        tc = asyncio.Task(lock.acquire(), loop=self.loop)
+        lock.release()
+        tb.cancel()
+        test_utils.run_briefly(self.loop)
+
+        self.assertTrue(lock.locked())
+        self.assertTrue(ta.done())
+        self.assertTrue(tb.cancelled())
+
     def test_release_not_acquired(self):
         lock = asyncio.Lock(loop=self.loop)