1

I have the following FastAPI application:

from fastapi import FastAPI
import socket

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}


@app.get("/healthcheck")
def health_check():
    result = some_network_operation()
    return result


def some_network_operation():
    HOST = "192.168.30.12" # This host does not exist so the connection will time out
    PORT = 4567

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(10)
        s.connect((HOST, PORT))
        s.sendall(b"Are you ok?")

        data = s.recv(1024)
        print(data)

This is a simple application with two routes:

  • / handler that is async
  • /healthcheck handler that is sync

With this particular example, if you call /healthcheck, it won't complete until after 10 seconds because the socket connection will timeout. However, if you make a call to / in the meantime, it will return the response right away because FastAPI's main thread is not blocked. This makes sense because according to the docs, FastAPI runs sync handlers on an external threadpool.

My question is, if it is at all possible for us to block the application (block FastAPI's main thread) by doing something inside the health_check method.

  • Perhaps by acquiring the global interpreter lock?
  • Some other kind of lock?
Dacite
  • 76
  • 4
  • Does this answer your question? [FastAPI runs api-calls in serial instead of parallel fashion](https://stackoverflow.com/questions/71516140/fastapi-runs-api-calls-in-serial-instead-of-parallel-fashion) – Chris Dec 01 '22 at 06:53
  • No, because in that case, the endpoint method is async whereas here its sync. – Dacite Dec 01 '22 at 07:38
  • I added a working example. However, it does not reproduce the problem. I am fairly certain the problem is indeed with a different section of the app. I guess I am trying to narrow down what may be causing it. – Dacite Dec 01 '22 at 08:35
  • I unfortunately cannot provide more details regarding the code. My issue is that my code do not work like the example. For instance, there is something in the `health_check` handler(of my actual application) that is causing all other routes in the application to block until the `health_check` function completes – Dacite Dec 01 '22 at 08:38
  • I guess mine was more of a theoretical question, so a minimal reproducible example is not always possible. If I had such example, it also means my question is answered. In the end, I did manage to reproduce it (see my answer) – Dacite Dec 02 '22 at 21:39

2 Answers2

1

Yes, if you try to do sync work in a async method it will block FastAPI, something like this:

@router.get("/healthcheck")
async def health_check():
    result = some_network_operation()
    return result

Where some_network_operation() is blocking the event loop because it is a synchronous method.

JarroVGIT
  • 4,291
  • 1
  • 17
  • 29
  • Yes, if it was an async method it will block FastAPI because async handlers run on the main thread. However, this is handler is sync. – Dacite Dec 01 '22 at 07:38
  • Yeah my answer was to your original question. The current version is very different than to the original one so my answer does not make a lot of sense anymore. – JarroVGIT Dec 02 '22 at 08:19
  • The essence of the question hasn't changed: `Is there a situation where a sync endpoint handler can block FastAPI?` was the previous question. Now it's just reworded to : `Can FastAPI guarantee a sync handler will never block the main application thread? `. However, I realize that the original question may not have been clear enough to provide good answers for. – Dacite Dec 02 '22 at 21:19
0

I think I may have an answer to my question, which is that there are some weird edge cases where a sync endpoint handler can block FastAPI.

For instance, if we adjust the some_network_operation in my example to the following, it will block the entire application.

def some_network_operation():
    """ No, this is not a network operation, but it illustrates the point """
    block = pow (363,100000000000000)

I reached this conclusion based on this question: pow function blocking all threads with ThreadPoolExecutor.

So, it looks like the GIL maybe the culprit here.

That SO question suggests using the multiprocessing module (which will get around GIL). However, I tried this, and it still resulted in the same behavior. So my root problem remains unsolved.

Either way, here is the entire example in the question edited to reproduce the problem:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}


@app.get("/healthcheck")
def health_check():
    result = some_network_operation()
    return result


def some_network_operation():
    block = pow(363,100000000000000)
Dacite
  • 76
  • 4