miguelgrinberg/microdot

Calling shutdown() throws error when there was no request made (using async WebSockets)

Muhlex opened this issue · 8 comments

Muhlex commented

Describe the bug
I am using the async version and am handling a single WebSocket endpoint. I want to gracefully shutdown the server when KeyboardInterrupting, however this results in an error only when no request was handled before this point in time.

Error:

Traceback (most recent call last):
  File "<stdin>", line 23, in <module>
  File "microdot_asyncio.py", line 325, in shutdown
  File "uasyncio/stream.py", line 1, in close
RuntimeError: can't cancel self

To Reproduce
Steps to reproduce the behavior:

from machine import Pin
from microdot_asyncio import Microdot
from microdot_asyncio_websocket import with_websocket

statusLed = Pin(2, Pin.OUT)
app = Microdot()

@app.route('/')
@with_websocket
async def index(request, ws):
	print("request in")
	while True:
		data = await ws.receive()
		for byte in data:
			print(byte)

try:
	statusLed.on()
	app.run(debug=True)
except KeyboardInterrupt:
	app.shutdown()
finally:
	statusLed.off()

When executing this script from a PC terminal and pressing CTRL+C before a request was made/socket connection established, the error appears and the server does not actually get shut down. After the connection is established, and also if it is closed afterwards, shutdown works fine.

Expected behavior
Graceful termination of the server, also when no request came in beforehand.

Additional context
I am using MicroPython with an ESP32.

This is not a bug, this is what the documentation says about shutdown():

Microdot provides a shutdown() method that can be invoked during the handling of a route to gracefully shut down the server when that request completes.

So basically, calling this function outside of a request is not a correct usage and is not supported. Also, if you interrupt the main thread with Ctrl-C then the web server is already interrupted, there is nothing to shutdown anymore.

Muhlex commented

I was wondering if I should file this as a Q&A, fearing that I'm missing something... Should have done that apparently, sorry! Thank you so much for the quick reply.

Alright, when shutdown is not supported outside of request handlers anyway, I'd like to use the opportunity to ask if there is a way to shutdown the server and freeing the port again "from the outside"? Can I accomplish this e. g. when running the server as a coroutine via start_server?

As I said above, once you are handling the KeyboardInterrupt the server isn't running anymore, so there is nothing to stop. I really don't understand why you are concerned about stopping the server. This is a single-threaded system, once your except block is executing, the server could not be executing at the same time, there are no threads or anything of that sort (unless you implemented something yourself to that effect).

Muhlex commented

You are probably right, I shouldn't worry about this issue too much. Would have been more of a convenience for development, because the port stays bound and I cannot re-execute the script without restarting the system. But I don't want to waste your time here, thanks for the info and fast replies!

How are you checking the state of the port? Or is it just that the bind() call fails the second time around?

Muhlex commented

Yes, binding the port simply fails with a EADDRINUSE the second time as it is still bound from the script that was terminated.

Okay, got it. You should have indicated that this is actually the problem, as that would have saved us all this time circling around the real issue.

A bound socket will take a bit of time to be recycled. I'd say after 2 or 3 minutes the socket should be available again, without having to power cycle the device. This is a standard annoyance of most (all?) TCP implementations and is not specific to MicroPython.

For the regular (non-async) case, I create the server with the SO_REUSEADDR option on the socket bind() call, which is the normal way to work around this problem. This is saying that the port should be bound even if it appears to be in use. But you are using the asyncio version, which creates the socket internally, without allowing the calling application to configure it (at least that is my understanding).

Muhlex commented

Alright, re-reading our conversation above I now see how I mislead you there.

So basically asyncio does not allow you to specify the socket binding options to re-use the "in-use" port, basically making it impossible to fix this problem in the scope of this library. Alright then. I don't really have any experience with async (Micro)Python, so I can't comment on the issue. – Unfortunately I also don't have time right now to look into this further to potentially contribute to a fix nevertheless.