Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tortoise crashes when used with Vercel and FastAPI #1684

Open
Thytu opened this issue Jul 28, 2024 · 1 comment
Open

Tortoise crashes when used with Vercel and FastAPI #1684

Thytu opened this issue Jul 28, 2024 · 1 comment

Comments

@Thytu
Copy link

Thytu commented Jul 28, 2024

Describe the bug
Tortoise seems to crash when used with Vercel (and probably any serverless service) + FastAPI.

When using tortoise-orm with FastAPI and Vercel, tortoise cannot manage to fetch the database connection resulting in the following error:

TypeError: 'NoneType' object is not iterable
Traceback (most recent call last):
  File "/var/task/vc__handler__python.py", line 315, in vc_handler
    response = asgi_cycle(__vc_module.app, body)
  File "/var/task/vc__handler__python.py", line 215, in __call__
    asyncio.run(self.run_asgi_instance(asgi_instance))
  File "/var/lang/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
  File "/var/lang/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
  File "/var/lang/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
  File "/var/task/vc__handler__python.py", line 219, in run_asgi_instance
    await asgi_instance
  File "/var/task/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/var/task/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/var/task/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "/var/task/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "/var/task/starlette/middleware/cors.py", line 85, in __call__
    await self.app(scope, receive, send)
  File "/var/task/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/var/task/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/var/task/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/var/task/starlette/routing.py", line 756, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/var/task/starlette/routing.py", line 776, in app
    await route.handle(scope, receive, send)
  File "/var/task/starlette/routing.py", line 297, in handle
    await self.app(scope, receive, send)
  File "/var/task/starlette/routing.py", line 77, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/var/task/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/var/task/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/var/task/starlette/routing.py", line 72, in app
    response = await func(request)
  File "/var/task/fastapi/routing.py", line 278, in app
    raw_response = await run_endpoint_function(
  File "/var/task/fastapi/routing.py", line 191, in run_endpoint_function
    return await dependant.call(**values)
  File "/var/task/Manager/main.py", line 48, in some_example
    await Video.all() # KO
  File "/var/task/tortoise/models.py", line 1262, in all
    return cls._db_queryset(using_db)
  File "/var/task/tortoise/models.py", line 1063, in _db_queryset
    db = using_db or cls._choose_db(for_write)
  File "/var/task/tortoise/models.py", line 1011, in _choose_db
    db = router.db_for_read(cls)
  File "/var/task/tortoise/router.py", line 36, in db_for_read
    return self._db_route(model, "db_for_read")
  File "/var/task/tortoise/router.py", line 31, in _db_route
    return connections.get(self._router_func(model, action))
  File "/var/task/tortoise/router.py", line 18, in _router_func
    for r in self._routers:

To Reproduce
Here is the code-snippet to reproduce it:

# app/main.py
from fastapi import FastAPI
from tortoise import Tortoise
from contextlib import asynccontextmanager
from tortoise.contrib.fastapi import RegisterTortoise
from tortoise import models, fields


# NOTE: this must be defined in
# a different file and then imported
class SomeTable(models.Model):
    id = fields.IntField(pk=True)


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with RegisterTortoise(
        app=app,
        config=YOUR_CONFIG_HERE,
        generate_schemas=True,
        add_exception_handlers=True,
    ):
        yield

    await Tortoise.close_connections()


app = FastAPI(lifespan=lifespan)


@app.get("/some-example")
async def some_example():
    await SomeTable.all() # TypeError: 'NoneType' object is not iterable
    return "some expected response"


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

Obviously, in order to reproduce the error one also needs to deploy it on Vercel :)

Expected behavior
I would expect tortoise to use previously initialized connection to return every elements of SomeTable.

Additional context
Note that in order to debug I also tried the following, which also results in a crash. However this time it crashs at the second call to the database as it tried to close an event loop that no-longer exists, resulting in a RuntimeError('Event loop is closed') error.

# app/main.py
from fastapi import FastAPI
from tortoise import Tortoise
from contextlib import asynccontextmanager
from tortoise.contrib.fastapi import RegisterTortoise
from tortoise import models, fields


# NOTE: this must be defined in
# a different file and then imported
class SomeTable(models.Model):
    id = fields.IntField(pk=True)


async def init_db():
    await Tortoise.init(config=YOUR_CONFIG_HERE)
    await Tortoise.generate_schemas()


app = FastAPI()


@app.get("/some-example")
async def some_example():
    await init_db() # First call works
    # second calls fail with `raise RuntimeError('Event loop is closed')``
    await SomeTable.all()
    return "some expected response"


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
@Thytu
Copy link
Author

Thytu commented Jul 29, 2024

Update: I found this temporary fix, however I'll obviously need to find a better option :D

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    await init_db() # already in the previous snippet

    response = await call_next(request)

    await Tortoise.close_connections() # Now also close the session after each call

    return response

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant