sanic-org / sanic

Accelerate your web app development | Build fast. Run fast.

Home Page:https://sanic.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

all workers are being blocked through the use of Unix sockets

carson0321 opened this issue · comments

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

In the following code, two APIs using Unix sockets are implemented. API B returns results easily, while API A requires a 10-second wait. When only API B is called, everything appears normal. However, an abnormal behavior is observed when API B is called after API A. Despite having four workers, API A blocks API B.

Demo: https://imgur.com/OepbE4u

Code snippet

import os
import time
from sanic import Sanic, HTTPResponse, Request, response

app = Sanic(__name__)

@app.get("/a")
def base(request: Request) -> HTTPResponse:
    print(f"PID: {os.getpid()}")
    time.sleep(10)
    return response.text("Done")

@app.get("/b")
def fast(request: Request) -> HTTPResponse:
    print(f"PID: {os.getpid()}")
    return response.text("Fast Done")

if __name__ == '__main__':
    import socket
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.bind('/tmp/api.sock')

    app.run(host="127.0.0.1", port=4444, workers=4, unix="/tmp/api.sock", debug=True)

Expected Behavior

Everything appears normal when calling API B after API A.

How do you run Sanic?

Sanic CLI

Operating System

Linux

Sanic Version

23.6.0

Additional context

No response

The issue has nothing to do with the use of your own socket object.

The blocking nature is related to two things

  1. your handlers are not async, therefore they will block the app completely and you can only handle one request at a time.

  2. inside your handlers you are using , time.sleep and not asyncio.sleep. The former is blocking, the latter not.

Sanic works on the premise of asyncio and therefore you may want to do a little research on how async Python works.

Hope this helps. Good luck.

Hi @ahopkins, thanks for your response. I understand that Python has async/await to handle asynchronous operations. However, it appears normal when modified as follows. In my opinion, this behavior is not normal, and it may have nothing to do with asynchronous operation.

import os
import time
from sanic import Sanic, HTTPResponse, Request, response

app = Sanic(__name__)

@app.get("/a")
def base(request: Request) -> HTTPResponse:
    print(f"PID: {os.getpid()}")
    time.sleep(10)
    return response.text("Done")

@app.get("/b")
def fast(request: Request) -> HTTPResponse:
    print(f"PID: {os.getpid()}")
    return response.text("Fast Done")

if __name__ == '__main__':
    app.run(host="127.0.0.1", port=4444, workers=4, debug=True)

Seems to work as expected, blocks for 10 seconds on /a, blocks until completed on b. Still blocks if async keyword is added because time.sleep() is a blocking call. same behavior over unix socket or tcp. If all workers are exhausted blocking on the /a route call, /b is in a pending state until a worker frees up to handle the route.

Can you clarify what you think is broken?

Demo1: https://imgur.com/OepbE4u
Demo2: https://imgur.com/qIy77rE

Hi @sjsadowski, It's my fault that the first original code snippet had the wrong section. I've edited it and noticed another issue that describes a situation reminiscent of this one: #2467 (comment).

I provide two demos using time.sleep(). However, Demo2 differs from Demo1 in that Sanic starts up without a unix socket object. During normal usage, when API B is called after API A, all workers function correctly, and you can observe the printed PID. In Demo1, only a single worker is utilized instead of multiple workers, despite having four workers available.

In summary, by replacing the 'unix' parameter with the 'sock' parameter, all workers function correctly. Do you believe this is the expected behavior?

app.run(host="127.0.0.1", port=4444, workers=4, unix="/tmp/api.sock", debug=True) -> blocked
app.run(host="127.0.0.1", port=4444, workers=4, sock=sock, debug=True) -> ok
app.run(host="127.0.0.1", port=4444, workers=4, debug=True) -> ok

I'll have to dig into the connection handler to identify whether that behavior should be expected. If it is, we should document it. I will update after I've had a chance to investigate.