starlette
starlette copied to clipboard
Handle websocket disconnect in event loop
It seems that the starlette.websockets.WebSocketDisconnect
exception is not raised when a client disconnects.
This can cause orphaned async tasks as multiple clients connect/disconnect. As someone first referenced on StackOverflow, it can also result in Uvicorn's shutdown/reload to block.
Example
#!/usr/bin/env python
import asyncio
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
app = Starlette()
@app.route('/')
async def index(req):
"""Return basic websocket connection template at base route"""
return HTMLResponse('''
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Example</title>
</head>
<body>
Intentionally empty. Messages echoed to js console.
<script>
ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = e => {
console.log(e);
};
</script>
</body>
</html>
''')
@app.websocket_route('/ws')
async def ws(websocket):
await websocket.accept()
try:
while True:
# Expected behavior: print "Looping..." until client
# disconnects, then close websocket
print(f'Looping. Client state: {websocket.client_state}')
# await websocket.send_text('Hello world!')
await asyncio.sleep(1)
finally:
await websocket.close()
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, log_level='debug')
After accessing the base route to create a websocket connection, closing the window/connection does not raise starlette.websockets.WebSocketDisconnect
, leaves the loop running indefinitely, and does not modify websocket.client_state
. Additional connections start additional loops.
If you add the line to send the Hello world! message over the websocket after the client has disconnected, websockets.exceptions.ConnectionClosedOK
will be raised and can be handled. However, this likely won't be a workaround for all use cases.
you can subclass WebSocketEndpoint
@benfasoli , here's an example of a dummy counter
@app.websocket_route("/counter")
class WebSocketTicks(WebSocketEndpoint):
async def on_connect(self, websocket: WebSocket) -> None:
await websocket.accept()
self.ticker_task = asyncio.create_task(self.tick(websocket))
logger.debug("connected")
async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
self.ticker_task.cancel()
logger.debug("disconnected")
async def on_receive(self, websocket: WebSocket, data: typing.Any) -> None:
await websocket.send_json({"Message: ": data})
async def tick(self, websocket: WebSocket) -> None:
counter = 0
while True:
logger.debug(counter)
await websocket.send_json({"counter": counter})
counter += 1
await asyncio.sleep(1)
@euri10 Thanks for the example.
Small issue: There's a solid 50-second delay between the actual (physical) disconnect and on_disconnect()
being called. Any way to make sure on_disconnect()
is called instantly?
I tested with the websockets library, and it has the same delay as well.
Seems very odd to me, I don't see any reason why there would be a 50s delay
Le jeu. 13 févr. 2020 à 11:24 PM, Dev Aggarwal [email protected] a écrit :
@euri10 https://github.com/euri10 Thanks for the example.
Small issue: There's a solid 50-second delay between the actual (physical) disconnect and on_disconnect() being called. Any way to make sure on_disconnect() is called instantly?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/encode/starlette/issues/759?email_source=notifications&email_token=AAINSPT5PCHK4DL22ASX5XLRCXCDBA5CNFSM4J4C7EYKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOELW23YA#issuecomment-586001888, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAINSPTIMB6XB2VMFZEUSDDRCXCDBANCNFSM4J4C7EYA .
Here's an MWE -
Grab 2 computers, run the server on one and client on the other. Then just disconnect the wifi on the client.
# testserver.py
from time import time
from starlette.applications import Starlette
from starlette.endpoints import WebSocketEndpoint
from starlette.websockets import WebSocket
app = Starlette(debug=True)
@app.websocket_route("/")
class MessagesEndpoint(WebSocketEndpoint):
async def on_connect(self, websocket):
await websocket.accept()
self.last_time = time()
print(f"[{self.last_time}] connected: {websocket.client}")
async def on_receive(self, websocket: WebSocket, data) -> None:
self.last_time = time()
print(f"[{self.last_time}] {data}")
async def on_disconnect(self, websocket, close_code):
print(f"[{time()}] disconnected: {websocket.client}")
print("delay:", time() - self.last_time)
$ uvicorn testserver:app --host 0.0.0.0
# testclient.py
import sys
from time import sleep
import websocket
ws = websocket.WebSocket()
ws.connect("ws://" + sys.argv[1])
while True:
ws.send("ping")
sleep(0.5)
$ python testclient.py <server ip>:8000
Logs from my server -
$ uvicorn testserver:app --host 0.0.0.0
INFO: Started server process [98786]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: ('192.168.31.50', 55556) - "WebSocket /" [accepted]
[1581665609.820539] connected: Address(host='192.168.31.50', port=55556)
[1581665609.824685] ping
[1581665610.420501] ping
[1581665610.929983] ping
[1581665611.446839] ping
[1581665611.830563] ping
[1581665612.363434] ping
[1581665612.975849] ping
[1581665613.387729] ping
[1581665613.900246] ping
[1581665614.4310899] ping
[1581665614.925177] ping
[1581665615.3376231] ping
[1581665615.9478881] ping
[1581665616.344498] ping
[1581665616.8673742] ping
[1581665659.828591] disconnected: Address(host='192.168.31.50', port=55556)
delay: 42.96127676963806
I was testing earlier with a flutter app, and the delay seems to be higher on that.
The delay does not exist on localhost, or if the client is gracefully shutdown with ctrl+c.
Then just disconnect the wifi on the client.
I'm not sure but is it the best way to disconnect the client ?
just in case I took your example, adapted it a bit (I didn't know what websocket lib it is in the client, so I rewrote a dummy client using websockets < note the s :dragon: ) and with it I seems to be ok after I hit ctrl+c on the client, which to me looks like a disconnection but maybe that's not what you have in mind.
testserver.py
# testserver.py
from time import time
import uvicorn
from starlette.applications import Starlette
from starlette.endpoints import WebSocketEndpoint
from starlette.websockets import WebSocket
app = Starlette(debug=True)
@app.websocket_route("/")
class MessagesEndpoint(WebSocketEndpoint):
encoding = "text"
last_time = 0
async def on_connect(self, websocket):
await websocket.accept()
print(f"[{time()}] connected: {websocket.client}")
async def on_receive(self, websocket: WebSocket, data) -> None:
self.last_time = float(data)
print(self.last_time)
async def on_disconnect(self, websocket, close_code):
print(f"[{time()}] disconnected: {websocket.client}")
print("delay:", time() - self.last_time)
if __name__ == '__main__':
uvicorn.run(app)
client
import asyncio
from time import time
import websockets
async def hello():
uri = "ws://127.0.0.1:8000"
async with websockets.connect(uri) as ws:
while True:
t = str(time())
await ws.send(t)
print(t)
await asyncio.sleep(0.5)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(hello())
output:
INFO: Started server process [42018]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
[1581666736.198393] connected: Address(host='127.0.0.1', port=59714)
1581666736.1990578
INFO: ('127.0.0.1', 59714) - "WebSocket /" [accepted]
1581666736.6998987
1581666737.2009335
1581666737.701966
[1581666737.944619] disconnected: Address(host='127.0.0.1', port=59714)
delay: 0.24270367622375488
INFO: ('127.0.0.1', 59760) - "WebSocket /" [accepted]
[1581666741.0054376] connected: Address(host='127.0.0.1', port=59760)
1581666741.0062165
1581666741.5072074
1581666742.008333
1581666742.509547
1581666743.0107918
[1581666743.2451403] disconnected: Address(host='127.0.0.1', port=59760)
delay: 0.23444056510925293
INFO: ('127.0.0.1', 59794) - "WebSocket /" [accepted]
[1581666744.2906296] connected: Address(host='127.0.0.1', port=59794)
1581666744.2912686
1581666744.792231
1581666745.2934177
[1581666745.720352] disconnected: Address(host='127.0.0.1', port=59794)
delay: 0.4269826412200928
Sorry, here's the library - websocket_client
$ pip install websocket_client
ctrl+c is not really representative of a real-world client IMO, because mobile clients may have bad internet connection, roam from one connection to other, or just plain disconnect WiFi anytime they want.
If there's no reliable way to tell if a client is connected or not, then even halfway-reliable message delivery can't be guaranteed :( (without a performance penalty, at least)
understood , the 40 to 50s you see is exactly what is described here: https://github.com/aaugustin/websockets/blob/3bab7fd155636c73b79b258de752b36687bba347/src/websockets/protocol.py#L803-L810
close_timeout default to 10s so maybe setting it to a lower value might help
It would be in uvicorn, not sure there's a way to override the timeout, @tomchristie might know
Following https://github.com/encode/uvicorn/issues/68, by using daphne, I was able to get the delay under 5 sec.
$ daphne -b 0.0.0.0 --ping-interval 1 --ping-timeout 1 testserver:app
2020-02-14 15:22:08,873 INFO Starting server at tcp:port=8000:interface=0.0.0.0
2020-02-14 15:22:08,873 INFO HTTP/2 support enabled
2020-02-14 15:22:08,873 INFO Configuring endpoint tcp:port=8000:interface=0.0.0.0
2020-02-14 15:22:08,874 INFO Listening on TCP address 0.0.0.0:8000
192.168.31.180:41250 - - [14/Feb/2020:15:22:33] "WSCONNECTING /" - -
192.168.31.180:41250 - - [14/Feb/2020:15:22:33] "WSCONNECT /" - -
[1581673953.148614] connected: Address(host='192.168.31.180', port=41250)
[1581673953.212526] ping
[1581673953.733991] ping
[1581673954.274146] ping
[1581673954.749561] ping
[1581673955.295857] ping
[1581673955.8084538] ping
[1581673956.322674] ping
[1581673956.8348138] ping
192.168.31.180:41250 - - [14/Feb/2020:15:22:39] "WSDISCONNECT /" - -
[1581673959.3946931] disconnected: Address(host='192.168.31.180', port=41250)
delay: 2.5599260330200195
Thank you for your time and attention BTW :)
Is there something we can do here in Starlette tho?
Thanks all for the discussion 🙏
Going to close this, since it seems out of scope for starlette to implement the requisite ping/pong methods to detect a websocket disconnect.
Hi all that how have the same problem with this code disconnect ... 1006 It helps me enourmous, anytime i have this problem and can´t work with ControlNet, now it works. Big Thankx to this community