You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During aiohttp usage we have noticed the strange issue - the client is sending PING message and the server takes a long time to respond. This simple script is the proof that pong method is correlated with the next message that is being sent
to client instead of being sent right away
To Reproduce
Run this simple script - ping_pong.py:
importargparseimportasyncioimportloggingimporttimeimporttracebackfromcontextvarsimportContextVarfromtypingimportAsyncGeneratorfromtypingimportAwaitablefromtypingimportCallablefromtypingimportOptionalfromtypingimportUnionfromuuidimportuuid4importaiohttpfromaiohttpimportClientSessionfromaiohttpimportClientWebSocketResponsefromaiohttpimportwebfromaiohttpimportWSMessagefromaiohttpimportWSMsgTypefromaiohttp.web_requestimportRequestfromaiohttp.web_wsimportWebSocketResponselogger=logging.getLogger(__name__)
RECONNECTION_TIMEOUT=1.0PING_SEND_TIME: ContextVar[dict[int, float]] =ContextVar('PING_SEND_TIME', default=dict())
SERVER_HOST="localhost"SERVER_PORT=8080asyncdefws_connect(
url: str, reconnect_timeout: float=RECONNECTION_TIMEOUT, autoping: bool=True
) ->tuple[ClientSession, ClientWebSocketResponse]:
whileTrue:
logger.info("Connection attempt...")
session=aiohttp.ClientSession()
try:
ws=awaitsession.ws_connect(url, autoping=autoping)
logger.info(f"Connected with autoping to set to {autoping}")
returnsession, wsexceptaiohttp.client.ClientConnectorError:
logger.info("CLOSING SESSION IN ws_connect!")
awaitsession.close()
awaitasyncio.sleep(reconnect_timeout)
asyncdefsend_ping(ws: Union[ClientWebSocketResponse, WebSocketResponse], ping_number: int) ->None:
message=f"{ping_number}"logger.debug(f"[PING {ping_number}] Sending ping with message = '{message}'")
awaitws.ping(message=message.encode("utf-8"))
PING_SEND_TIME.get()[ping_number] =time.time()
logger.debug(f"[PING {ping_number}] Successfully sent ping with message = '{message}'")
asyncdefstream_from_ws(url: str, sink: Callable[[str], Awaitable[None]]) ->None:
session, ws=awaitws_connect(url=url, reconnect_timeout=1, autoping=False)
try:
awaitfetch_stream_with_ping(ws=ws, sink=sink)
exceptasyncio.CancelledError:
traceback.print_exc()
finally:
awaitsession.close()
asyncdefwait_for_pong(
ws: Union[ClientWebSocketResponse, WebSocketResponse], timeout: int=20
) ->Optional[WSMessage]:
listener_id=str(uuid4())
logger.debug(f"[{listener_id}] Listening for PONG started!")
try:
response=awaitasyncio.wait_for(ws.receive(), timeout=timeout)
exceptasyncio.TimeoutError:
logger.error(f"[{listener_id}] PONG was never received and TimeoutError was raised!")
returnNoneifresponse.type==WSMsgType.PONG:
pong_number=int(response.data.decode('utf-8'))
logger.debug(f"[{listener_id}] PONG was successfully received - message = '{pong_number}'")
pong_receive_time=time.time()
ping_send_time=PING_SEND_TIME.get()[pong_number]
logger.info(f"Client was waiting for PONG for - {pong_receive_time-ping_send_time}!")
delPING_SEND_TIME.get()[pong_number]
returnNoneelse:
logger.debug(f"[{listener_id}] Received response that is not PONG")
returnresponseasyncdefack(data: WSMessage) ->str:
return"ok"asyncdeffetch_stream_with_ping(
ws: Union[ClientWebSocketResponse, WebSocketResponse], sink: Callable[[str], Awaitable[None]]
) ->None:
ping_number=0whileTrue:
# send PING and wait for PONGawaitsend_ping(ws, ping_number)
ping_number+=1response1=awaitwait_for_pong(ws, timeout=20) # this response should be PONGresponse2=awaitwait_for_pong(ws, timeout=20) # this should be normal WS eventresponses= [response1, response2]
true_events= [responseforresponseinresponsesifresponseisnotNone]
foreventintrue_events:
awaitsink(event.data)
awaitws.send_str(awaitack(event))
asyncdefsend_stream(
ws: Union[ClientWebSocketResponse, WebSocketResponse],
stream: AsyncGenerator[str, None],
get_message_func: Callable[[], Awaitable[WSMessage]],
send_timeout: float=1.0,
ack_timeout: float=1.0,
) ->None:
asyncdefreceive_and_ack() ->None:
ack_response=awaitget_message_func()
awaitack(ack_response)
asyncdefsend_message(msg: str) ->None:
awaitasyncio.wait_for(ws.send_str(data=msg), timeout=send_timeout)
awaitasyncio.wait_for(receive_and_ack(), timeout=ack_timeout)
sleep_time=0asyncformessageinstream:
logger.info(f"[SERVER] Sending message = '{message}'")
awaitsend_message(message)
awaitasyncio.sleep(sleep_time)
sleep_time= (sleep_time+1) %5returnNoneasyncdefwebsocket_handler(request: Request) ->web.WebSocketResponse:
ws=web.WebSocketResponse(autoping=True)
awaitws.prepare(request)
stream=events_stream(100)
awaitsend_stream(ws, stream, get_message_func=ws.receive)
asyncformsginws:
ifmsg.type==aiohttp.WSMsgType.TEXT:
ifmsg.data=='close':
awaitws.close()
else:
awaitws.send_str(msg.data+'/answer')
elifmsg.type==aiohttp.WSMsgType.ERROR:
logger.error(f"ws connection closed with exception {ws.exception()}")
logger.info('Websocket connection closed')
returnwsasyncdefwebsocket_handler_custom_ping(request: Request) ->web.WebSocketResponse:
ws=web.WebSocketResponse(autoping=False)
awaitws.prepare(request)
stream=events_stream(100)
message_queue: asyncio.Queue[WSMessage] =asyncio.Queue()
asyncdefconsume_events(web_socket: web.WebSocketResponse, queue: asyncio.Queue[WSMessage]) ->None:
asyncformsginws:
ifmsg.type==aiohttp.WSMsgType.PING:
message=msg.data.decode("utf-8")
logger.debug(f"[SERVER] Received PING message = '{message}'")
awaitweb_socket.pong(msg.data)
elifmsg.type==aiohttp.WSMsgType.TEXT:
ifmsg.data=='close':
awaitws.close()
else:
awaitqueue.put(msg)
elifmsg.type==aiohttp.WSMsgType.ERROR:
logger.error(f"ws connection closed with exception {ws.exception()}")
raisews.exception()
events_consumption_task=asyncio.create_task(consume_events(ws, message_queue))
stream_sending_task=asyncio.create_task(send_stream(ws, stream, get_message_func=message_queue.get))
awaitasyncio.gather(events_consumption_task, stream_sending_task)
logger.info('Websocket connection closed')
returnwsasyncdefawait_termination() ->None:
# By design aiohttp server do not hang:# https://docs.aiohttp.org/en/stable/web_advanced.html#application-runnerswhileTrue:
awaitasyncio.sleep(3600.0)
asyncdefserver() ->None:
app=web.Application(logger=logger)
app.add_routes([web.get('/ws-normal', websocket_handler)])
app.add_routes([web.get('/ws-custom', websocket_handler_custom_ping)])
app_runner=web.AppRunner(app)
try:
awaitapp_runner.setup()
site=web.TCPSite(app_runner, SERVER_HOST, SERVER_PORT)
awaitsite.start()
logger.info(f"Successfully started server {SERVER_HOST}:{SERVER_PORT}")
awaitawait_termination()
finally:
logger.info(f"Cleaning up the server")
awaitapp_runner.cleanup()
asyncdefclient(endpoint: str) ->None:
asyncdefclient_sink(message: str) ->None:
logger.info(f"[CLIENT] Received message = '{message}'")
awaitstream_from_ws(url=f"ws://{SERVER_HOST}:{SERVER_PORT}/ws-{endpoint}", sink=client_sink)
asyncdefevents_stream(events_number: int) ->AsyncGenerator[str, None]:
foriinrange(events_number):
yieldf"Message {i} from events stream"awaitasyncio.sleep(1)
asyncdefmain(client_endpoint: str) ->None:
server_task=asyncio.create_task(server())
client_task=asyncio.create_task(client(client_endpoint))
awaitasyncio.gather(server_task, client_task)
if__name__=="__main__":
parser=argparse.ArgumentParser(description='Show PING-PONG issue in aiohttp')
parser.add_argument('--client-endpoint', default="normal", help="Define which endpoint client should use")
parser.add_argument('--logging-level', default="INFO", help="Logging level as in standard Python logging library")
args=parser.parse_args()
endpoint=args.client_endpointlogging_level=logging._nameToLevel[args.logging_level]
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging_level)
asyncio.run(main(endpoint))
Expected behavior
We are using receive-acknowledge protocol:
Client is establishing Websocket connection with the server by doing simple request
Server is preparing event stream for the client
Server is sending one message at a time and then is waiting for ACK message
Client is receiving the main message and sending ok string as ACK response
Server, after receiving ACK knows that the next message can be sent to client
PING-PONG
At any given time client can send PING message to server
If the connection is established server should respond with PONG right away
Logs/tracebacks
By running `python ping_pong.py --client-endpoint=normal --logging-level=INFO` you can see that with `autoping=True`
the PONG messages are correlated with the speed of server-stream.
python ping_pong.py --client-endpoint=normal --logging-level=INFO
2022-09-06 10:56:58,417:INFO:Connection attempt...
2022-09-06 10:56:58,423:INFO:Successfully started server localhost:8080
2022-09-06 10:56:58,425:INFO:[SERVER] Sending message = 'Message 0 from events stream'
2022-09-06 10:56:58,425:INFO:Connected with autoping to set to False
2022-09-06 10:56:58,426:INFO:Client was waiting for PONG for - 0.0004050731658935547!
2022-09-06 10:56:58,426:INFO:[CLIENT] Received message = 'Message 0 from events stream'
2022-09-06 10:56:59,427:INFO:[SERVER] Sending message = 'Message 1 from events stream'
2022-09-06 10:56:59,429:INFO:Client was waiting for PONG for - 1.0026228427886963!
2022-09-06 10:56:59,429:INFO:[CLIENT] Received message = 'Message 1 from events stream'
2022-09-06 10:57:01,432:INFO:[SERVER] Sending message = 'Message 2 from events stream'
2022-09-06 10:57:01,434:INFO:Client was waiting for PONG for - 2.004383087158203!
2022-09-06 10:57:01,434:INFO:[CLIENT] Received message = 'Message 2 from events stream'
2022-09-06 10:57:04,437:INFO:[SERVER] Sending message = 'Message 3 from events stream'
2022-09-06 10:57:04,438:INFO:Client was waiting for PONG for - 3.004377841949463!
2022-09-06 10:57:04,438:INFO:[CLIENT] Received message = 'Message 3 from events stream'
2022-09-06 10:57:08,440:INFO:[SERVER] Sending message = 'Message 4 from events stream'
2022-09-06 10:57:08,441:INFO:Client was waiting for PONG for - 4.002522945404053!
2022-09-06 10:57:08,441:INFO:[CLIENT] Received message = 'Message 4 from events stream'
By running separate coroutine that responds right-away I am able to overcome this issue but it does feel like a hack:
`python ping_pong.py --client-endpoint=custom --logging-level=INFO`
```text
python ping_pong.py --client-endpoint=custom --logging-level=INFO
2022-09-06 10:57:35,876:INFO:Connection attempt...
2022-09-06 10:57:35,882:INFO:Successfully started server localhost:8080
2022-09-06 10:57:35,883:INFO:[SERVER] Sending message = 'Message 0 from events stream'
2022-09-06 10:57:35,883:INFO:Connected with autoping to set to False
2022-09-06 10:57:35,884:INFO:Client was waiting for PONG for - 0.0005218982696533203!
2022-09-06 10:57:35,884:INFO:[CLIENT] Received message = 'Message 0 from events stream'
2022-09-06 10:57:35,885:INFO:Client was waiting for PONG for - 0.0004208087921142578!
2022-09-06 10:57:36,886:INFO:[SERVER] Sending message = 'Message 1 from events stream'
2022-09-06 10:57:36,887:INFO:[CLIENT] Received message = 'Message 1 from events stream'
2022-09-06 10:57:36,888:INFO:Client was waiting for PONG for - 0.0006530284881591797!
2022-09-06 10:57:38,890:INFO:[SERVER] Sending message = 'Message 2 from events stream'
2022-09-06 10:57:38,891:INFO:[CLIENT] Received message = 'Message 2 from events stream'
2022-09-06 10:57:38,892:INFO:Client was waiting for PONG for - 0.00048804283142089844!
2022-09-06 10:57:41,894:INFO:[SERVER] Sending message = 'Message 3 from events stream'
2022-09-06 10:57:41,895:INFO:[CLIENT] Received message = 'Message 3 from events stream'
2022-09-06 10:57:41,896:INFO:Client was waiting for PONG for - 0.0007541179656982422!
2022-09-06 10:57:45,897:INFO:[SERVER] Sending message = 'Message 4 from events stream'
2022-09-06 10:57:45,898:INFO:[CLIENT] Received message = 'Message 4 from events stream'
2022-09-06 10:57:45,899:INFO:Client was waiting for PONG for - 0.0006568431854248047!
### Python Version
```console
$ python --version
Python 3.9.12
I've not looked into it, but that's quite a lengthy reproducer. So, first thing would be to try and convert that into the simplest test you can and add it to our test suite. If the fix isn't obvious, we can merge the test with xfail first and come back to the solution later.
Describe the bug
During aiohttp usage we have noticed the strange issue - the client is sending PING message and the server takes a long time to respond. This simple script is the proof that
pong
method is correlated with the next message that is being sentto client instead of being sent right away
To Reproduce
Run this simple script -
ping_pong.py
:Expected behavior
We are using receive-acknowledge protocol:
ok
string as ACK responsePING-PONG
Logs/tracebacks
aiohttp Version
multidict Version
yarl Version
OS
macOS
Related component
Server, Client
Additional context
No response
Code of Conduct
The text was updated successfully, but these errors were encountered: