1

I'm new to Python and I'm working on a FastAPI & peewee application. I want to manage the database connection pool explicitly so according to the framework integration documentation for FastAPI I use the startup/shutdown events.

database = PooledSqliteDatabase('foobar.db', autoconnect=False, stale_timeout=60)

with database:
  database.create_tables([models.Foobar])

app = FastAPI()

app.include_router(foobar_router)

@app.on_event("startup")
def startup():
  database.connect()

@app.on_event("shutdown")
def shutdown():
  if not database.is_closed():
    database.close()

The problem is that these events do not correspond to "open the connection when a request is received, then close it when the response is returned", but when the whole app starts and stops. More importantly I don't understand why I get "Error, database connection not opened." when I access the database in my router as it should still be opened at that point? It only seems to work when I add async to my routes.

I guess I could switch to some proper "before/after request" middleware, but at this point I'm wondering if my approach is just completely wrong.

Edit: I tried using middleware, but while this unsurprisingly works as intended, I still have the issue that I get "Error, database connection not opened." unless I add async to my routes.

@app.middleware("http")
async def with_database(request: Request, call_next):
  with database:
    response = await call_next(request)
  return response
Steffen
  • 1,328
  • 3
  • 12
  • 29

1 Answers1

2

Note your pool probably won't work as expected in an asyncio environment. Peewee's connection model, including the pool, is built around a thread-per-connection. With async the connections are pooled, but since everything is running in the same thread, all your coroutines will be sharing the same connection. This doesn't present any problems for standard threaded or gevent code (which patches thread local to be green-thread local).

That said, I don't use FastAPI but extrapolating from their (insane) sqlalchemy doc, you might try the following approach if you want to avoid a per-request middleware:

from fastapi import Depends, FastAPI, HTTPException


app = FastAPI()

database = PooledSqliteDatabase('foobar.db', autoconnect=False, stale_timeout=60)

with database:
    database.create_tables([models.Foobar])

def ensure_connection():
    database.connect()
    try:
        yield database
    finally:
        database.close()

@app.post("/users/", ...)
def create_user(..., database=Depends(ensure_connection)):
    ...

I think a per-request middleware is probably cleaner to implement, however. Given my caveat at the beginning of the comment, I'd strongly suggest you analyze your application and verify that it's using connections properly -- very likely (and this will depend on when you yield to the event loop) it is not, though.

coleifer
  • 24,887
  • 6
  • 60
  • 75
  • I read about these issues while debugging this and it's just way over my head to be honest. My assumption for now was that as long as I don't use `async` in my routes, pooling would work as intented, is that wrong? I don't have an application yet, I have to port one to Python, so I want to figure out the best practice way to use a simple ORM (the application is *very* small) with FastAPI. I agree that the middleware is probably cleaner, I'm just confused why the official documentation seems to have this incorrect information (which usually means I misunderstood it) and why I get this error. – Steffen Sep 14 '22 at 15:08
  • 1
    Regarding "as long as I don't use async in my routes, pooling would work as intented" - I don't know enough about FastAPI under-the-hood to be able to tell you. I can tell you that it works perfectly well with Flask, and all the other non-async cousins. – coleifer Sep 14 '22 at 20:38
  • To shed some light on [`def` vs `async def`](https://stackoverflow.com/a/71517830/17865804), "When you declare a path operation function with normal `def` instead of `async def`, it is run in an external threadpool that is then awaited, instead of being called directly (as it would block the server)". – Chris Sep 15 '22 at 12:58
  • Thanks Chris. Given that's the case, then it sounds like a middleware is necessary, since the threads used by the pool are not known or visible to the caller. – coleifer Sep 15 '22 at 15:39
  • Thanks a lot @coleifer, what a great answer and reference for the project I'm currently working on! I am bookmarking this! – SkyWalker Oct 02 '22 at 11:31