Skip to content

Commit ae4a4e4

Browse files
asyncio: Preserve contextvars across SelectorThread on Windows (#3479)
contextvars that were set on the main thread at event loop creation need to be preserved across callbacks that pass through the SelectorThread.
1 parent 197ff13 commit ae4a4e4

File tree

2 files changed

+44
-3
lines changed

2 files changed

+44
-3
lines changed

tornado/platform/asyncio.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import asyncio
2626
import atexit
2727
import concurrent.futures
28+
import contextvars
2829
import errno
2930
import functools
3031
import select
@@ -472,6 +473,8 @@ class SelectorThread:
472473
_closed = False
473474

474475
def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None:
476+
self._main_thread_ctx = contextvars.copy_context()
477+
475478
self._real_loop = real_loop
476479

477480
self._select_cond = threading.Condition()
@@ -491,7 +494,8 @@ async def thread_manager_anext() -> None:
491494
# clean up if we get to this point but the event loop is closed without
492495
# starting.
493496
self._real_loop.call_soon(
494-
lambda: self._real_loop.create_task(thread_manager_anext())
497+
lambda: self._real_loop.create_task(thread_manager_anext()),
498+
context=self._main_thread_ctx,
495499
)
496500

497501
self._readers: Dict[_FileDescriptorLike, Callable] = {}
@@ -618,7 +622,9 @@ def _run_select(self) -> None:
618622
raise
619623

620624
try:
621-
self._real_loop.call_soon_threadsafe(self._handle_select, rs, ws)
625+
self._real_loop.call_soon_threadsafe(
626+
self._handle_select, rs, ws, context=self._main_thread_ctx
627+
)
622628
except RuntimeError:
623629
# "Event loop is closed". Swallow the exception for
624630
# consistency with PollIOLoop (and logical consistency

tornado/test/asyncio_test.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# under the License.
1212

1313
import asyncio
14+
import contextvars
1415
import threading
1516
import time
1617
import unittest
@@ -25,8 +26,14 @@
2526
to_asyncio_future,
2627
AddThreadSelectorEventLoop,
2728
)
28-
from tornado.testing import AsyncTestCase, gen_test, setup_with_context_manager
29+
from tornado.testing import (
30+
AsyncTestCase,
31+
gen_test,
32+
setup_with_context_manager,
33+
AsyncHTTPTestCase,
34+
)
2935
from tornado.test.util import ignore_deprecation
36+
from tornado.web import Application, RequestHandler
3037

3138

3239
class AsyncIOLoopTest(AsyncTestCase):
@@ -261,3 +268,31 @@ def test_tornado_accessor(self):
261268
asyncio.set_event_loop_policy(self.AnyThreadEventLoopPolicy())
262269
self.assertIsInstance(self.executor.submit(IOLoop.current).result(), IOLoop)
263270
self.executor.submit(lambda: asyncio.get_event_loop().close()).result() # type: ignore
271+
272+
273+
class SelectorThreadContextvarsTest(AsyncHTTPTestCase):
274+
ctx_value = "foo"
275+
test_endpoint = "/"
276+
tornado_test_ctx = contextvars.ContextVar("tornado_test_ctx", default="default")
277+
tornado_test_ctx.set(ctx_value)
278+
279+
def get_app(self) -> Application:
280+
tornado_test_ctx = self.tornado_test_ctx
281+
282+
class Handler(RequestHandler):
283+
async def get(self):
284+
# On the Windows platform,
285+
# when a asyncio.events.Handle is created
286+
# in the SelectorThread without providing a context,
287+
# it will copy the current thread's context,
288+
# which can lead to the loss of the main thread's context
289+
# when executing the handle.
290+
# Therefore, it is necessary to
291+
# save a copy of the main thread's context in the SelectorThread
292+
# for creating the handle.
293+
self.write(tornado_test_ctx.get())
294+
295+
return Application([(self.test_endpoint, Handler)])
296+
297+
def test_context_vars(self):
298+
self.assertEqual(self.ctx_value, self.fetch(self.test_endpoint).body.decode())

0 commit comments

Comments
 (0)