简体   繁体   中英

Why does getting a contextvar here not work?

So here's the basic code (sorry it's long)

import argparse
import asyncio
from contextvars import ContextVar
import sys

# This thing is the offender
message_var = ContextVar("message")


class ServerProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info("peername")
        print("Server: Connection from {}".format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print("Server: Data received: {!r}".format(message))

        print("Server: Send: {!r}".format(message))
        self.transport.write(data)

        print("Server: Close the client socket")
        self.transport.close()


class ClientProtocol(asyncio.Protocol):
    def __init__(self, on_conn_lost):
        self.on_conn_lost = on_conn_lost
        self.transport = None
        self.is_connected: bool = False

    def connection_made(self, transport):
        self.transport = transport
        self.is_connected = True

    def data_received(self, data):
        # reading back supposed contextvar
        message = message_var.get()

        print(f"{message} : {data.decode()}")

    def connection_lost(self, exc):
        print("The server closed the connection")
        self.is_connected = False
        self.on_conn_lost.set_result(True)

    def send(self, message: str):
        # Setting context var
        message_var.set(message)
        if self.transport:
            self.transport.write(message.encode())

    def close(self):
        self.transport.close()
        self.is_connected = False
        if not self.on_conn_lost.done():
            self.on_conn_lost.set_result(True)


async def get_input(client: ClientProtocol):
    loop = asyncio.get_running_loop()
    while client.is_connected:
        message = await loop.run_in_executor(None, input, ">>>")
        if message == "q":
            client.close()
            return
        client.send(message)


async def main(args):
    host = "127.0.0.1"
    port = 5001

    loop = asyncio.get_running_loop()

    if args.server:
        server = await loop.create_server(lambda: ServerProtocol(), host, port)
        async with server:
            await server.serve_forever()
        return

    on_conn_lost = loop.create_future()
    client = ClientProtocol(on_conn_lost)
    await loop.create_connection(lambda: client, host, port)
    await get_input(client)


if __name__ == "__main__":

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--server", "-s", default=False, action="store_true", help="Start server"
    )
    arguments = parser.parse_args(sys.argv[1:])
    asyncio.run(main(args=arguments))

This crashes with the following exception:

Exception in callback _ProactorReadPipeTransport._loop_reading(<_OverlappedF...shed result=4>)
handle: <Handle _ProactorReadPipeTransport._loop_reading(<_OverlappedF...shed result=4>)>
Traceback (most recent call last):
  File "C:\Users\brent\AppData\Local\Programs\Python\Python310\lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\Users\brent\AppData\Local\Programs\Python\Python310\lib\asyncio\proactor_events.py", line 320, in _loop_reading
    self._data_received(data, length)
  File "C:\Users\brent\AppData\Local\Programs\Python\Python310\lib\asyncio\proactor_events.py", line 270, in _data_received
    self._protocol.data_received(data)
  File "E:\Development\Python\ibcs2023\_prep\experimental\asyncio_context.py", line 40, in data_received
    message = message_var.get()
LookupError: <ContextVar name='message' at 0x0000023F30A54FE0>
The server closed the connection

Why does calling message = message_var.get() cause a crash? Why can't Python find the context var? Why is data_received not in the same context as send ? How can I keep them in the same context?

I'm working on a larger project with the main branch of Textual and it uses a contextvar that loses context every time a message is received using a modified version of the code above.

Keeping a separated "context" for each task is exactly what contextvars are about. You could only assert that the send and data_received methods were called within the same context if you had control over the "uperlying" (as opposed to 'underlying') driver of your Protocol class - that is not the case, and both are called in different contexts. I mean, the answer to "How can I keep them in the same context?" is: you can't unless you write your own implementation of the code which makes this work inside asyncio.

There is no way you can keep track of metadata from a message, and retrieve this metadata on getting the reply, unless there is a marker on the message itself, that will survive the round-trip. That is: your networking/communication protocol itself have to spec a way to identify messages It might be as simple as a sequential integer number prefixing every string, for example - or, in this case where you simply echo the message back, it could be the message itself. Once you have that, a simple dictionary having these message IDs as keys, will work for what you seem to intend in this example.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM