4

I want to count the number of requests in a specific URL path.

app = FastAPI()
counter = 0

@app.get("/do_something")
async def do_something():
    global counter
    counter += 1
    return {"message": "Hello World"}

Is this code ok? The counter should be thread safe? asincio safe? Is that the right way to count requests (Without DB)? Is there a meaning for the "async" in the "do_something" function in this situation? And how to make it work with several workers?

idan ahal
  • 707
  • 8
  • 21

3 Answers3

4

This code is unsafe because you are not using locks. I think you thought that the += operation is atomic, so it's safe to use without locks, but it's not. To protect your state you need locks. The asyncio library provides locks https://docs.python.org/3/library/asyncio-sync.html.

import asyncio

app = FastAPI()
counter = 0
lock = asyncio.Lock()

@app.get("/do_something")
async def do_something():
    global counter

    async with lock:
        counter += 1
        # some other thread-safe code here

    return {"message": "Hello World"}
Marat Mkhitaryan
  • 844
  • 2
  • 11
  • 25
3

If you're using 1 worker you ushould use an asyncio lock because fastAPI is asynchronous.

import asyncio

app = FastAPI()
counter_lock = asyncio.Lock()
counter = 0

@app.get("/do_something")
async def do_something():
    global counter

    async with counter_lock:
        counter += 1

    return {"message": "Hello World"}

But if there is more than 1 worker that wouldn't work because they do not share the same memory. Should be using a cache mechanism or a DB, As explained here.

idan ahal
  • 707
  • 8
  • 21
1

According to This issue, non-async endpoints will be processed in a thread pool.

Non-async def endpoints (i.e., plain def endpoints) get executed in a threadpool, so it is possible to run into thread safety issues if you make modifications to shared global objects or similar.

If it is that it uses a thread lock, then yeah, that's fine. It will prevent any other thread from touching that thing until the first one finishes, so that's fine.

So the code could be like this:

import threading

app = FastAPI()
counter = 0
lock = threading.Lock()

@app.get("/do_something")
def do_something():
    global counter

    with lock:
        counter += 1
        # some other thread-safe code here

    return {"message": "Hello World"}
Lee
  • 63
  • 6