miguelgrinberg/microdot

Clients blocking in Sync

codiak6335 opened this issue · 11 comments

I'm unsure if I'm going to describe a bug or the expected behavior of sync mode. While working on some bidirectional routes between two RP2040 I discovered some blocking IO when I used my phone as a 2nd client.

The first RP2040 function as an access point while the 2nd RP2040 connects to the SSID as a client.

[SERVER CODE]
import network
from microdot_asyncio import Microdot, redirect, send_file, Response
SSID = 'somesid'
KEY = '123456789'
ap = network.WLAN(network.AP_IF)
ap.config(essid=SSID, password=KEY) 
ap.active(True)   
while not ap:
    pass
app = Microdot()
@app.route('/somefunction/<someparm>')
def registerDevice(request, someparm):
    return f'"msg":"received {someparm}"'
app.run(debug=True, port=80)
[CLIENT CODE]
import urequests
import network
SSID = 'somesid'
KEY = '123456789'
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
    sta_if.active(True)
    sta_if.connect(SSID, KEY)
    while not sta_if.isconnected():
        pass
x = 1
while True:
    url = f"http://192.168.4.1/somefunction/{x}"
    response = urequests.get(url)
    if response.status_code != 200:
        break
    print(response.text, response.status_code)
    response.close()
    x+=1

Things run great when you start the server and then the client.
The issue is when I connect my phone to the SSID and make a request... once the request is made, the server will respond to the next request from the RP2040 and then block until I make a request from the phone again... it's one-for-one alternating between the two client devices. This leads me to believe the server is blocking the socket read without checking for existing data in the buffer.
Just starting to look at the Microdot code but figured I post here before getting too off track.

When using the microdot_asyncio module I don't see the issue. I had planned on using _thread in my project and not sure about mixing it with asyncio

The only option for concurrency in the RP2040 is asyncio. With the sync server the device can handle one request at a time.

I started playing with Async last night and was creating a race condition easily.. going to have to spend more time with that version.

To be clear with SYNC, the app could handle only a single request was/is expected. That it can only handle a single remote connection is what has me confused. Each client, making atomic requests that are completed in the order they arrive is the exact behavior I am expecting.

It can handle a single remote connection at a time. It should be able to handle your two clients but not at the same time. I assume this is the behavior that you observed?

I'm other words, any blocking is temporary, while the server completes a previous request, but there shouldn't be any permanent blocking. Nothing that requires the web server to be restarted. Correct?

You have the expected behavior correct.

That is not what is happening. 2 clients can connect to the hotspot... one and continually call a microdot provided route without issue.. the 2nd client makes a request and gets a response and reply, and then the 1st client gets a single reply... but now it blocks to timeouts... unless i force a request from the second client

How is the second client sending a request?

Second client is a samsung phone connectes to the rp20 hotspot using chrome web browser.

Spent some time with different scenarios:

  • using 3 different RPiP-Ws 1 as AP, 2 as clients SYNC works as expected. Both clients start and stop at will without issue (each client sleeps 5 seconds between requests).
  • added my mobile phone to the mix, using chrome after a single route request the system hangs. Hanging in this case is blocks after the request is handled. ` the create_thread returns from the chrome request and there is an immediate sock accepted from the chrome browser, the code continues to the handle_request and blocks on waiting on Readline
while not self.shutdown_requested:
            try:
                sock, addr = self.server.accept()
            except OSError as exc:  # pragma: no cover
                if exc.errno == errno.ECONNABORTED:
                    break
                else:
                    print_exception(exc)
            except Exception as exc:  # pragma: no cover
                print_exception(exc)
            else:
                create_thread(self.handle_request, sock, addr)
  • Attempted the same test using Chrome from my laptop and got the same results.
  • Firefox from my laptop worked fine for this scenario as it's not sending a 'ghost' request

So it appears that Chrome or some plugin is creating an unexpected connection that doesn't have an end-of-line marker.
Async would likely be a workaround.

@codiak6335 Did you try disabling keep alive in your response? Add a Connection header with the value close to the response of your endpoint.

Sorry for the delay... connection close did not change the behavior

That is really the only suggestion I can make. If the client keeps the connection alive there is little the server can do to prevent it.