]> granicus.if.org Git - python/commitdiff
Fix waiter cancellation in asyncio.Lock (#1031) (#2037)
authorYury Selivanov <yury@magic.io>
Fri, 9 Jun 2017 21:07:48 +0000 (17:07 -0400)
committerGitHub <noreply@github.com>
Fri, 9 Jun 2017 21:07:48 +0000 (17:07 -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
Misc/NEWS

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)
 
index 580f5793a1e94c1b52975669954c0820e6b005a8..0342c71cd65a131bcfb6578b151fab6871c01470 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -49,6 +49,9 @@ Core and Builtins
 Library
 -------
 
+- bpo-27585: Fix waiter cancellation in asyncio.Lock.
+  Patch by Mathieu Sornay.
+
 - bpo-30418: On Windows, subprocess.Popen.communicate() now also ignore EINVAL
   on stdin.write() if the child process is still running but closed the pipe.