Running an asyncio server from a pytest fixture - python

I'm trying to run a server using asyncio in a pytest fixture
#pytest.fixture(autouse=True)
#pytest.mark.asyncio
async def start_endpoints(
endpoint1: ServerEndpoint,
endpoint2: ServerEndpoint
):
pool = ThreadPoolExecutor(max_workers=2)
loop = asyncio.get_running_loop()
await loop.run_in_executor(pool, endpoint1.start)
await loop.run_in_executor(pool, endpoint2.start)
The start method is like the following
async def start(self):
try:
server = await asyncio.start_server(self.handle_req, self.addr, self.port)
addr = server.sockets[0].getsockname()
print(f'{self.name}: serving on {addr}')
async with server:
await server.serve_forever()
Whereas the test prints this error once it tries to open a connection with the server
self = <_WindowsSelectorEventLoop running=False closed=False debug=False>
fut = <Future finished exception=ConnectionRefusedError(10061, "Connect call failed ('127.0.0.1', 9000)")>
sock = <socket.socket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6>
address = ('127.0.0.1', 9000)
def _sock_connect_cb(self, fut, sock, address):
if fut.cancelled():
return
try:
err = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
if err != 0:
# Jump to any except clause below.
> raise OSError(err, f'Connect call failed {address}')
E ConnectionRefusedError: [Errno 10061] Connect call failed ('127.0.0.1', 9000)
EDIT:
The problem is that the event loop is closed right after so I tried to mark all my fixture with (scope="module") but now I get
ScopeMismatch: You tried to access the 'function' scoped fixture 'event_loop' with a 'module' scoped request object, involved factories
test\e2e\test_peer.py:380: def start_endpoints
EDIT2:
So I added the event_loop fixture
#pytest.fixture(scope="module")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()
that should override the default loop for each fixture using #pytest.mark.asyncio.
#pytest.fixture(autouse=True, scope="module")
#pytest.mark.asyncio
async def start_endpoints(
event_loop,
endpoint1: ServerEndpoint,
endpoint2: ServerEndpoint
):
pool = ThreadPoolExecutor(max_workers=2)
await event_loop.run_in_executor(pool, endpoint1.start)
await event_loop.run_in_executor(pool, endpoint2.start)
By debugging inside my test the event_loop is equal to the loop that I'm storing inside the ServerEndpoint (that is asyncio.get_running_loop()) but I'm still getting the ConnectionRefusedError

Probably it should works like this:
async def handle(reader, writer):
data = await reader.read(100)
message = data.decode()
print(f"SERVER: Received {message!r}")
writer.write(data)
await writer.drain()
print(f"SERVER: Sent: {message!r}")
writer.close()
print("SERVER: Closed the connection")
async def start():
server = await asyncio.start_server(handle, host, port)
addr = server.sockets[0].getsockname()
print(f'Server is running on {addr[0:2]}')
async with server:
await server.serve_forever()
async def _async_wait_for_server(event_loop, host, port):
while True:
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
await event_loop.sock_connect(a_socket, (host, port))
return
except ConnectionRefusedError:
await asyncio.sleep(0.01)
finally:
a_socket.close()
#pytest.fixture()
def server(event_loop, host, port):
cancel_handle = asyncio.ensure_future(start(host, port), loop=event_loop)
event_loop.run_until_complete(
asyncio.wait_for(_async_wait_for_server(event_loop, host, port), 5.0)
)
try:
yield
finally:
cancel_handle.cancel()
But I recommend you make functional tests in another way:
create docker image for server and image for tests
create docker-compose.yml file (use depends_on and healthcheck for tests container)
run tests after starting server

Related

Async function blocking main thread

Hello I am wanting to create a client socket via python, and I found this example (https://stackoverflow.com/a/49918082/12354066). The only problem I am wondering about is, I have a whole other program I want to implement this with, and it seems loop.run_until_complete(asyncio.wait(tasks)) is blocking the whole thread and not allowing me to execute any more functions i.e print(1) after loop.run_until_complete(asyncio.wait(tasks)). I want to be able to listen & send messages, but I also want to be able to execute other after I begin listening, maybe this is better suited for threads and not async (I don't know much async..)
import websockets
import asyncio
class WebSocketClient():
def __init__(self):
pass
async def connect(self):
'''
Connecting to webSocket server
websockets.client.connect returns a WebSocketClientProtocol, which is used to send and receive messages
'''
self.connection = await websockets.client.connect('ws://127.0.0.1:8765')
if self.connection.open:
print('Connection stablished. Client correcly connected')
# Send greeting
await self.sendMessage('Hey server, this is webSocket client')
return self.connection
async def sendMessage(self, message):
'''
Sending message to webSocket server
'''
await self.connection.send(message)
async def receiveMessage(self, connection):
'''
Receiving all server messages and handling them
'''
while True:
try:
message = await connection.recv()
print('Received message from server: ' + str(message))
except websockets.exceptions.ConnectionClosed:
print('Connection with server closed')
break
async def heartbeat(self, connection):
'''
Sending heartbeat to server every 5 seconds
Ping - pong messages to verify connection is alive
'''
while True:
try:
await connection.send('ping')
await asyncio.sleep(5)
except websockets.exceptions.ConnectionClosed:
print('Connection with server closed')
break
main:
import asyncio
from webSocketClient import WebSocketClient
if __name__ == '__main__':
# Creating client object
client = WebSocketClient()
loop = asyncio.get_event_loop()
# Start connection and get client connection protocol
connection = loop.run_until_complete(client.connect())
# Start listener and heartbeat
tasks = [
asyncio.ensure_future(client.heartbeat(connection)),
asyncio.ensure_future(client.receiveMessage(connection)),
]
loop.run_until_complete(asyncio.wait(tasks))
print(1) # never gets executed

Why pytest hangs while testing simple async function?

I have a simple echo server function. Everything works fine if I launch pytest with a single test, but if I launch it with two tests then the second one hangs on waiting for the server to start, and I can't understand why. Here's a complete code.
The server:
async def handle_echo(reader, writer):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print(f"SERVER: Received {message!r} from {addr!r}")
writer.write(data)
await writer.drain()
print(f"SERVER: Sent: {message!r}")
writer.close()
print("SERVER: Closed the connection")
Test setup:
HOST = "localhost"
#pytest.fixture()
def event_loop():
return asyncio.get_event_loop()
async def _async_wait_for_server(event_loop, addr, port):
while True:
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
await event_loop.sock_connect(a_socket, (addr, port))
return
except ConnectionRefusedError:
await asyncio.sleep(0.001)
finally:
a_socket.close()
#pytest.fixture()
def server(event_loop):
unused_tcp_port = 65432
cancel_handle = asyncio.ensure_future(main(unused_tcp_port), loop=event_loop)
event_loop.run_until_complete(asyncio.wait_for(
_async_wait_for_server(event_loop, HOST, unused_tcp_port), 5.0))
try:
yield unused_tcp_port
finally:
cancel_handle.cancel()
async def main(port):
server = await asyncio.start_server(handle_echo, HOST, port)
addr = server.sockets[0].getsockname()
print(f'SERVER: Serving on {addr[0:2]}')
async with server:
await server.serve_forever()
Tests:
#pytest.mark.asyncio
async def test_something(server):
message = "Foobar!"
reader, writer = await asyncio.open_connection(HOST, server)
print(f'CLIENT: Sent {message!r}')
writer.write(message.encode())
await writer.drain()
data = await reader.read(100)
print(f'CLIENT: Received {data.decode()!r}')
print('CLIENT: Close the connection')
writer.close()
await writer.wait_closed()
#pytest.mark.asyncio
async def test_another_thing(server):
# Literally the same
Pytest output:
================================================================== test session starts ===================================================================
platform win32 -- Python 3.7.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- C:\Users\hurrd.virtualenvs\chatServer-iuEQ-ghN\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\hurrd\PycharmProjects\chatServer
plugins: asyncio-0.16.0
collected 2 items
tests/unit/test_themes.py::test_something PASSED [ 50%]
tests/unit/test_themes.py::test_another_thing

how to setup python asyncio websocket server to gracefully loose connection with a client

I'm relatively new to websocket and asyncio and is struggling to gracefully handle the scenario when my server looses a connection.
My server code is:
#!/usr/bin/env python
# https://websockets.readthedocs.io/en/stable/intro.html
import asyncio
import websockets
import socket
USERS = set()
def users_event():
return str(len(USERS))
async def notify_users():
if USERS: # asyncio.wait doesn't accept an empty list
message = users_event()
await asyncio.wait([user.send(message) for user in USERS])
async def register(websocket):
USERS.add(websocket)
print('Current number of users:',len(USERS))
await notify_users()
async def unregister(websocket):
USERS.remove(websocket)
print('Current number of users:',len(USERS))
await notify_users()
async def consumer_handler(websocket, path):
try:
async for message in websocket:
ip = websocket.remote_address[0]
print('From ip ', ip, ' RECEIVED message: ', message)
except asyncio.exceptions.CancelledError:
print('Could not complete receiving')
except websockets.exceptions.ConnectionClosedError:
print('Connection unexpectly closed')
finally:
await unregister(websocket)
async def sendcommands(websocket, path):
ip = websocket.remote_address[0]
await register(websocket)
try:
while True:
await asyncio.sleep(2)
await websocket.send("test")
except asyncio.exceptions.CancelledError:
print('Could not complete sending')
except websockets.exceptions.ConnectionClosedError:
print('Connection unexpectly closed')
except websockets.exceptions.ConnectionClosed:
print('Connection closed during sending')
finally:
await unregister(websocket)
async def handler(websocket, path):
try:
consumer_task = asyncio.ensure_future(
consumer_handler(websocket, path))
producer_task = asyncio.ensure_future(
sendcommands(websocket, path))
done, pending = await asyncio.wait(
[consumer_task, producer_task],
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel()
except asyncio.exceptions.CancelledError:
print('Caught unfinished task')
if __name__ == '__main__':
serverport = 8765 # just a number that is assumed used by no other process
hostname = socket.gethostname()
IPAddr = socket.gethostbyname(hostname)
start_server = websockets.serve(handler, IPAddr, serverport)
try:
asyncio.get_event_loop().run_until_complete(start_server)
print('Server started')
asyncio.get_event_loop().run_forever()
except KeyboardInterrupt: # CTRL-C
print("Received exit, existing")
When the server start and the client connects, the server gives the following output to terminal:
Server started
Current number of users: 1
From ip 192.168.50.132 RECEIVED message: [WSc] Connected
From ip 192.168.50.132 RECEIVED message: A test message
I create a connection loss by pulling the plug on the client. The server gives the following output to terminal 40s after I pulled the plug:
Connection unexpectly closed
Current number of users: 0
Connection unexpectly closed
Task exception was never retrieved
future: <Task finished name='Task-10' coro=<sendcommands() done, defined at .\websocketserver.py:47> exception=KeyError(<websockets.server.WebSocketServerProtocol object at 0x000001E8617A09D0>)>
Traceback (most recent call last):
File ".\websocketserver.py", line 61, in sendcommands
await unregister(websocket)
File ".\websocketserver.py", line 29, in unregister
USERS.remove(websocket)
KeyError: <websockets.server.WebSocketServerProtocol object at 0x000001E8617A09D0>
My questions are:
what is the reason for the KeyError?
what is meant by "Task exception was never retrieved"?
why do I get the messages after 40s? I guess a default timeout is set somewhere to 40s. Would it be a benefit with a shorter timeout duration?
Beside answering my questions, any suggestions for improving server robustness will be highly appreciated. The answers help me get my head around this setup because documentation of websockets+asyncio server in python is sparse.

Socket leak with wait_for in asyncio with python3.6 and python3.7

In some cases asyncio.wait_for can lead to socket leak.
Condensed example:
async def _create_connection(timeout=60, ssl_obj):
loop = asyncio.get_event_loop()
connector = loop.create_connection(MyEchoClientProtocol, '127.0.0.1', 5000, ssl=ssl_obj)
connector = asyncio.ensure_future(connector)
tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
return tr, pr
async def main():
...
res = await asyncio.wait_for(_acquire_impl(), timeout=timeout, loop=loop)
If my understanding is correct wait_for should work in exactly 2 ways
the inner task is completed and the outer task will receive the result – transport and protocol in this case
The inner task is cancelled and no connection was established
I provided source code for client and server so the problem can be easily reproduced on your system.
certificate and key can be easily generated with minica
I found out that if I catch CancelledError and add a done_callback to the inner task, like so:
try:
tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
return tr, pr
except asyncio.CancelledError as e:
connector.add_done_callback(_done_callback)
raise e
then inside of _done_callback I can access the transport and protocol object and close the transport manually to prevent leaking:
Client
import asyncio
import io
import struct
import functools
import ssl as ssl_module
import socket
import collections
import time
import traceback
class MyEchoClientProtocol(asyncio.Protocol):
def connection_made(self, transport):
print('connection_made', transport)
query = 'hello world'
transport.write(query.encode('latin-1'))
def data_received(self, data):
print('data_received', data)
def connection_lost(self, exc):
print('connection_lost', exc)
async def create_connection(ssl_obj, timeout=60):
loop = asyncio.get_event_loop()
connector = loop.create_connection(MyEchoClientProtocol, '127.0.0.1', 5000, ssl=ssl_obj)
connector = asyncio.ensure_future(connector)
tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
return tr, pr
async def main(timeout, ssl_obj):
async def _acquire_impl():
try:
proxy = await create_connection(ssl_obj)
except Exception:
raise
else:
return proxy
res = await asyncio.wait_for(_acquire_impl(), timeout=timeout, loop=loop)
return res
async def test_cancel():
sc = ssl_module.create_default_context(ssl_module.Purpose.SERVER_AUTH, cafile='localhostc.crt')
sc.check_hostname = False
sc.verify_mode = ssl_module.CERT_NONE
for i in range(10): # try 50 times
timeout = 0.003
try:
tr, pr = await main(
timeout=timeout, ssl_obj=sc
)
tr.close()
except asyncio.TimeoutError as e:
print('timeouterror', repr(e))
await asyncio.sleep(600)
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(test_cancel())
loop.run_forever()
loop.close()
Server
import asyncio
import ssl
async def handle_echo(reader, writer):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print("Received %r from %r" % (message, addr))
print("Send: %r" % message)
writer.write(data)
await writer.drain()
loop = asyncio.get_event_loop()
sc = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
sc.load_cert_chain('localhost.crt', 'localhost.key')
coro = asyncio.start_server(handle_echo, '127.0.0.1', 5000, loop=loop, ssl=sc)
server = loop.run_until_complete(coro)
try:
loop.run_forever()
except KeyboardInterrupt:
pass
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
I run netstat -a | grep 5000 | grep ESTAB | awk '{ print $5 }' | sort | uniq -c | grep 5000 after the script is done and there are many unclosed connections.
The output depends on your hardware so you might need to tweak the timeout parameter
The Questions
Why can this happen?
Is it an expected behaviour? Maybe I'm missing something fundamental about asyncio and python.
What is correct method to prevent leaking sockets while using timeout?
Environment
OS: Ubuntu 16.04 LTS
python: Python 3.6.6 or Python 3.7.3

how to handle tcp client socket auto reconnect in python asyncio?

I am using python asyncio streams to connect to several socket servers, but when the server is down, my code can't auto reconnect.
What I need is that, when the server is down, my script will try to reconnect every 5 seconds, until connected and start to parse the data again.
import asyncio
server1 = {'host': '192.168.1.51', 'port': 11110}
server2 = {'host': '192.168.1.52', 'port': 11110}
async def tcp_client(host, port, loop):
print('connect to server {} {}'.format(host, str(port)))
reader, writer = await asyncio.open_connection(host, port, loop=loop)
while True:
data = await reader.read(100)
print('raw data received: {}'.format(data))
await asyncio.sleep(0.1)
loop = asyncio.get_event_loop()
try:
for server in [server1, server2]:
loop.run_until_complete(tcp_client(server['host'], server['port'], loop))
print('task added: connect to server {} {}'.format(server['host'], server['port']))
finally:
loop.close()
print('loop closed')
You can handle reconnection by simply looping over a try/except statement.
Additionally, asyncio.wait_for can be used to set a timeout on the read operation.
Consider this working example:
import asyncio
async def tcp_client(host, port):
reader, writer = await asyncio.open_connection(host, port)
try:
while not reader.at_eof():
data = await asyncio.wait_for(reader.read(100), 3.0)
print('raw data received: {}'.format(data))
finally:
writer.close()
async def tcp_reconnect(host, port):
server = '{} {}'.format(host, port)
while True:
print('Connecting to server {} ...'.format(server))
try:
await tcp_client(host, port)
except ConnectionRefusedError:
print('Connection to server {} failed!'.format(server))
except asyncio.TimeoutError:
print('Connection to server {} timed out!'.format(server))
else:
print('Connection to server {} is closed.'.format(server))
await asyncio.sleep(2.0)
async def main():
servers = [('localhost', 8888), ('localhost', 9999)]
coros = [tcp_reconnect(host, port) for host, port in servers]
await asyncio.gather(*coros)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

Categories