]> granicus.if.org Git - python/commitdiff
bpo-34769: Thread safety for _asyncgen_finalizer_hook(). (GH-9716)
authortwisteroid ambassador <twisteroidambassador@users.noreply.github.com>
Tue, 9 Oct 2018 15:30:21 +0000 (23:30 +0800)
committerYury Selivanov <yury@magic.io>
Tue, 9 Oct 2018 15:30:21 +0000 (11:30 -0400)
Lib/asyncio/base_events.py
Lib/test/test_asyncio/test_base_events.py
Misc/NEWS.d/next/Library/2018-10-09-11-01-16.bpo-34769.cSkkZt.rst [new file with mode: 0644]

index 780a06192dcf8c743f72eb577df3ad1d3cadd657..3726c556d4f09df424fcb1566cc8a1219d09ab7f 100644 (file)
@@ -477,10 +477,7 @@ class BaseEventLoop(events.AbstractEventLoop):
     def _asyncgen_finalizer_hook(self, agen):
         self._asyncgens.discard(agen)
         if not self.is_closed():
-            self.create_task(agen.aclose())
-            # Wake up the loop if the finalizer was called from
-            # a different thread.
-            self._write_to_self()
+            self.call_soon_threadsafe(self.create_task, agen.aclose())
 
     def _asyncgen_firstiter_hook(self, agen):
         if self._asyncgens_shutdown_called:
index d15a9c6a81399a7c113e73098f99ad92bf158145..6d544d1eda86354f545d6dfbd5d9d616379c4e7e 100644 (file)
@@ -926,6 +926,74 @@ class BaseEventLoopTests(test_utils.TestCase):
         self.loop.run_forever()
         self.loop._selector.select.assert_called_once_with(0)
 
+    async def leave_unfinalized_asyncgen(self):
+        # Create an async generator, iterate it partially, and leave it
+        # to be garbage collected.
+        # Used in async generator finalization tests.
+        # Depends on implementation details of garbage collector. Changes
+        # in gc may break this function.
+        status = {'started': False,
+                  'stopped': False,
+                  'finalized': False}
+
+        async def agen():
+            status['started'] = True
+            try:
+                for item in ['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR']:
+                    yield item
+            finally:
+                status['finalized'] = True
+
+        ag = agen()
+        ai = ag.__aiter__()
+
+        async def iter_one():
+            try:
+                item = await ai.__anext__()
+            except StopAsyncIteration:
+                return
+            if item == 'THREE':
+                status['stopped'] = True
+                return
+            asyncio.create_task(iter_one())
+
+        asyncio.create_task(iter_one())
+        return status
+
+    def test_asyncgen_finalization_by_gc(self):
+        # Async generators should be finalized when garbage collected.
+        self.loop._process_events = mock.Mock()
+        self.loop._write_to_self = mock.Mock()
+        with support.disable_gc():
+            status = self.loop.run_until_complete(self.leave_unfinalized_asyncgen())
+            while not status['stopped']:
+                test_utils.run_briefly(self.loop)
+            self.assertTrue(status['started'])
+            self.assertTrue(status['stopped'])
+            self.assertFalse(status['finalized'])
+            support.gc_collect()
+            test_utils.run_briefly(self.loop)
+            self.assertTrue(status['finalized'])
+
+    def test_asyncgen_finalization_by_gc_in_other_thread(self):
+        # Python issue 34769: If garbage collector runs in another
+        # thread, async generators will not finalize in debug
+        # mode.
+        self.loop._process_events = mock.Mock()
+        self.loop._write_to_self = mock.Mock()
+        self.loop.set_debug(True)
+        with support.disable_gc():
+            status = self.loop.run_until_complete(self.leave_unfinalized_asyncgen())
+            while not status['stopped']:
+                test_utils.run_briefly(self.loop)
+            self.assertTrue(status['started'])
+            self.assertTrue(status['stopped'])
+            self.assertFalse(status['finalized'])
+            self.loop.run_until_complete(
+                self.loop.run_in_executor(None, support.gc_collect))
+            test_utils.run_briefly(self.loop)
+            self.assertTrue(status['finalized'])
+
 
 class MyProto(asyncio.Protocol):
     done = None
diff --git a/Misc/NEWS.d/next/Library/2018-10-09-11-01-16.bpo-34769.cSkkZt.rst b/Misc/NEWS.d/next/Library/2018-10-09-11-01-16.bpo-34769.cSkkZt.rst
new file mode 100644 (file)
index 0000000..fc034c9
--- /dev/null
@@ -0,0 +1,2 @@
+Fix for async generators not finalizing when event loop is in debug mode and
+garbage collector runs in another thread.