Search code examples
pythonpython-3.xpython-asynciopython-3.11python-3.12

Async server and client scripts stopped working after upgrading to Python3.12


So I have two scripts that use asyncio's servers' for communication, the script's work by the server opening an asyncio server and listening for connections, the client script connecting to that server, the server script stopping listening for new connections and assigning the reader and the writer to global variables so data sending and receiving would be possible.

Server.py:

import asyncio
import sys


class Server:
    def __init__(self):
        self.reader, self.writer = None, None
        self.connected = False

    async def listen(self, ip: str, port: int) -> None:
        """
        Listens for incoming connections and handles the first connection.
        After accepting the first connection, it stops the server from accepting further connections.

        :param ip: IP address to listen on.
        :param port: Port number to listen on.
        """

        async def handle_connection(reader, writer):
            print("Client connected!")
            # Assign the reader and writer to instance variables for later use
            self.reader, self.writer = reader, writer
            self.connected = True

            print("Shutting down server from accepting new connections")
            server.close()
            await server.wait_closed()


        print(f"Listening on {ip}:{port}")
        server = await asyncio.start_server(handle_connection, ip, port)

        try:
            async with server:
                await server.serve_forever()

        except KeyboardInterrupt:
            sys.exit(1)

        except asyncio.CancelledError:
            print("Connection canceled")
        except Exception as e:
            print(f"Unexpected error while trying to listen, Error: {e}")
            sys.exit(1)


if __name__ == '__main__':
    server = Server()
    asyncio.run(server.listen('192.168.0.35', 9090))

Client.py:

import asyncio

class Client:
    def __init__(self):
        self.reader, self.writer = None, None
        self.connected = False

    async def connect(self, ip: str, port: int) -> None:
        """
        Connects to a server at the specified IP address and port.

        :param ip: IP address of the server.
        :param port: Port number of the server.
        """

        while not self.connected:
            try:
                self.reader, self.writer = await asyncio.wait_for(
                    asyncio.open_connection(ip, port), 5
                )
                print(f"Connecting to {ip}:{port}")
                self.connected = True
                break
            except Exception as e:
                print(
                    f"Failed to connect to {ip}:{port} retrying in 10 seconds."
                )
                print(e)
                await asyncio.sleep(10)
                continue


if __name__ == '__main__':
    Client = Client()
    asyncio.run(Client.connect('192.168.0.35', 9090))

In python 3.11 the execution process was as follows; the Client script is connecting to the listening server script, the server script is calling the handle_connection function and the function is raising asyncio.CancelledError which exits the listening method and keeps the reader and writer alive.

However in python 3.12; the Client script is connecting to the listening server script, the server script is calling the handle_connection and is being stuck at await server.wait_closed().

I did some debugging and discovered that the await server.wait_closed() line is not returning unless the writer is closed using writer.close(), which we do not want because as I said the script will be using the reader and writer for communication.

My intended action was for the server script to listen to a single connection, when a connection is established for it to stop listening for any further connection attempts but still maintain a connection between it and the original connected client.

EDIT: I upgraded from python3.11.9 to python3.12.6


Solution

  • To stop serving server.close does the job. The wait_closed has a broader meaning. Let me quote directly from the asyncio code, it explains everything, also why it was working on 3.11:

        async def wait_closed(self):
            """Wait until server is closed and all connections are dropped.
    
            - If the server is not closed, wait.
            - If it is closed, but there are still active connections, wait.
    
            Anyone waiting here will be unblocked once both conditions
            (server is closed and all connections have been dropped)
            have become true, in either order.
    
            Historical note: In 3.11 and before, this was broken, returning
            immediately if the server was already closed, even if there
            were still active connections. An attempted fix in 3.12.0 was
            still broken, returning immediately if the server was still
            open and there were no active connections. Hopefully in 3.12.1
            we have it right.
            """