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

Fix Pydantic ORM mode blocking the event loop #888

Merged
merged 1 commit into from
Jan 18, 2020
Merged

Conversation

tiangolo
Copy link
Member

@tiangolo tiangolo commented Jan 18, 2020

🐛 Fix FastAPI serialization of Pydantic ORM mode blocking the event loop.

Description

In the current implementation, if a path operation function returns a DB object with relations, and it is then serialized by Pydantic in ORM mode using response_model, the serialization is done in the main loop. And Pydantic with ORM mode would go and use the getters to retrieve the relationships (as it should, this is not Pydantic's fault), but that's a potentially (and most probably) I/O blocking operation, while it reads from the DB.

Test it

To test the problem, run this sample application with Uvicorn:

import time
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel


class DBItem:
    def __init__(self):
        self.name = "John"
        self.age = 20

    @property
    def pets(self):
        time.sleep(15)
        return ["Mr. Fluff", "Doge"]


class Item(BaseModel):
    name: str
    age: int
    pets: List[str]

    class Config:
        orm_mode = True


app = FastAPI()


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


@app.get("/item", response_model=Item)
def read_item():
    item = DBItem()
    return item

It simulates a (very long) blocking operation with a time.sleep().

Open several tabs at the same time with the docs UI and do 1 request to /item and the rest to /.

The request to /item will take 15 seconds to load (as it should). But the other requests will also take 15 seconds to load while they should be instantaneous. This is because they are all running in the same main loop.

Fix it

Then apply this fix and try again. /item will take 15 seconds (as it should) but additional requests to / will be instantaneous.

How it works

Whenever a path operation function uses normal def instead of async def, the model serialization is done in a threadpool.

There are no checks to see if the model uses ORM mode or not, as it's possible that the top-level model doesn't use ORM mode but a deeply nested sub-model could be using ORM mode.

@codecov
Copy link

codecov bot commented Jan 18, 2020

Codecov Report

Merging #888 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #888   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files         292    292           
  Lines        7663   7665    +2     
=====================================
+ Hits         7663   7665    +2
Impacted Files Coverage Δ
fastapi/routing.py 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3a5158a...70e3115. Read the comment docs.

@tiangolo tiangolo merged commit 7cea84b into master Jan 18, 2020
@tiangolo tiangolo deleted the pydantic-orm-block branch January 18, 2020 16:33
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

Successfully merging this pull request may close these issues.

1 participant