Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions tornado/platform/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import asyncio
import atexit
import concurrent.futures
import contextvars
import errno
import functools
import select
Expand Down Expand Up @@ -472,6 +473,8 @@ class SelectorThread:
_closed = False

def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None:
self._main_thread_ctx = contextvars.copy_context()

self._real_loop = real_loop

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

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

try:
self._real_loop.call_soon_threadsafe(self._handle_select, rs, ws)
self._real_loop.call_soon_threadsafe(
self._handle_select, rs, ws, context=self._main_thread_ctx
)
except RuntimeError:
# "Event loop is closed". Swallow the exception for
# consistency with PollIOLoop (and logical consistency
Expand Down
37 changes: 36 additions & 1 deletion tornado/test/asyncio_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# under the License.

import asyncio
import contextvars
import threading
import time
import unittest
Expand All @@ -25,8 +26,14 @@
to_asyncio_future,
AddThreadSelectorEventLoop,
)
from tornado.testing import AsyncTestCase, gen_test, setup_with_context_manager
from tornado.testing import (
AsyncTestCase,
gen_test,
setup_with_context_manager,
AsyncHTTPTestCase,
)
from tornado.test.util import ignore_deprecation
from tornado.web import Application, RequestHandler


class AsyncIOLoopTest(AsyncTestCase):
Expand Down Expand Up @@ -261,3 +268,31 @@ def test_tornado_accessor(self):
asyncio.set_event_loop_policy(self.AnyThreadEventLoopPolicy())
self.assertIsInstance(self.executor.submit(IOLoop.current).result(), IOLoop)
self.executor.submit(lambda: asyncio.get_event_loop().close()).result() # type: ignore


class SelectorThreadContextvarsTest(AsyncHTTPTestCase):
ctx_value = "foo"
test_endpoint = "/"
tornado_test_ctx = contextvars.ContextVar("tornado_test_ctx", default="default")
tornado_test_ctx.set(ctx_value)

def get_app(self) -> Application:
tornado_test_ctx = self.tornado_test_ctx

class Handler(RequestHandler):
async def get(self):
# On the Windows platform,
# when a asyncio.events.Handle is created
# in the SelectorThread without providing a context,
# it will copy the current thread's context,
# which can lead to the loss of the main thread's context
# when executing the handle.
# Therefore, it is necessary to
# save a copy of the main thread's context in the SelectorThread
# for creating the handle.
self.write(tornado_test_ctx.get())

return Application([(self.test_endpoint, Handler)])

def test_context_vars(self):
self.assertEqual(self.ctx_value, self.fetch(self.test_endpoint).body.decode())