LoginSignup
8
9

More than 3 years have passed since last update.

転職黙示録 (10) FastAPIのソースを読む 第4回 UvicornとFastAPIの関係

Posted at

Uvicoronの日

対象コード

とりあえずHello World的なプログラムを走らせてみる.

main.py
import uvicorn
from fastapi import FastAPI

app = FastAPI()

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

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

目的

サーバーであるUvicornとアプリケーションのFastAPIの関係を考察する.

Q. appとは何か?

 最初にUvicornが期待するアプリケーションの仕様, つまりASGIインターフェースとはどんなものか確認しておきましょう.

  • scop
  • receive
  • send

 ASGIに適合するアプリケーションはこれらの引数を取るcallableなネイティブ・コルーティンであれば良いようです. Quick Startにはこんなコードがあります.

async def app(scope, receive, send):
    assert scope['type'] == 'http'
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ]
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, world!',
    })

これがどこかにあるかを探すわけです.

Q. Uvicornサーバーの起動

 Uvicoronを実行するとServerクラスのrunメソッドをrun関数か呼び出されて起動します.

uvicorn/main.py
def run(app, **kwargs):
    ...
        server.run()

内部ではイベント・ループを始動しています.

uvicorn/main.py
def run(self, sockets=None, shutdown_servers=True):
    self.config.setup_event_loop()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(self.serve(sockets=sockets))

 実はconfigのsetup_event_loopではuvloopというasyncioとは違うイベント・ループのモジュールが読み込まれています. 前回紹介したポリシーを入れ替えることで実現しているようです. 提供されている機能にはちゃんと使途があるんだなと実感できます.

uvicorn/loops/uvloop.py
def uvloop_setup():
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

 後は同じでイベント・ループを起動してrun_until_completeでサーバーを非同期に実行します. serveメソッドでは以下のメソッドを呼び出ししています.

  • startup
  • main_loop
  • shutdown

 startupも重要そうですが, main_loopが本題です. main_loopでリクエストを受け待ち処理します. デバッガーで実行するとクライアントからリクエストを送らないと延々にループを繰り返しますが, 試しにブラウザからアクセスするとHttpToolsProtocolに飛ばされます.

Q. HttpToolsProtocolとは?

  この辺はUvicornというよりasyncioの話と関係するようです.

 何れにしてもこのプロトコルのメソッドが呼ばれるんですがon_headers_completeというメソッドでアプリケーションが実行されたりします.

uvicorn/protocols/http/httptools_impl.py
task = self.loop.create_task(self.cycle.run_asgi(app))
task.add_done_callback(self.tasks.discard)
self.tasks.add(task)

Q. run_asgiとは

 run_asgiはタスクとしてスケジューリングされます. つまり一回呼び出し元に戻るわけです. それはどこかというとコールスタックを見るとrun_until_completになります. コールスタックの流れは以下のようになります.

  • uvicorn.run(app, host="0.0.0.0", port=8000)
  • server.run()
  • loop.run_until_complete(self.serve(sockets=sockets))
  • run_asgi

 こうしてみると分かりやすいですね:relaxed:
 
 appはFastAPIのインスタンスです. つまりrun_asgiというrunnerコルーティンがFastAPIアプリケーションをイベント・ループを使ってスケジュール実行していることになります. run_asgiはRequestResponseCycleというクラスのメソッドでUvicornが提供するモジュールです. RequestResponseCycleは以下のようなメソッドを持ちます.

  • run_asgi
  • send
  • receive

 イニシャライザではscopeをもらっていますので, このクラス自身がASGIインターフェースに必要なデータと処理を一通り持っています. 実際run_asgiでは以下のようにappとのやりとりを開始しますが, その際に自身のメソッドを渡しています.

uvicorn/protocols/http/h11_impl.py
result = await app(self.scope, self.receive, self.send)

 ファイル名からこれがhttp1.1サーバーの実装であることも伺えます. これでとりあえずUvicornサーバーとFastAPI(Starlett)アプリケーションの関係が見えてきました.

Q. Router/APIRouterとは?

 ここからはFastAPIやStarletteといったアプリケーションの話ですがせっかくだから追いかけて見ます. このコルーティンの呼び出しを辿ってみましょう.

  • Starlette
  • ServerErrorMiddleware
  • ExceptionMiddleware
  • Router/APIRouter

 途中例外処理のミドルウェアを挟んでいますが面白いことにRouterを継承したクラスもASGIのインターフェースをもつcallbleとして呼び出し可能になっていることです.

いよいよルートとスコープの中にあるパスをマッチします. scopeは上から渡されたデータでrouteはルーターが保持しているのでした.

for route in self.routes:
    match, child_scope = route.matches(scope)
    if match == Match.FULL:
        scope.update(child_scope)
        await route(scope, receive, send) # (*)
        return
    elif match == Match.PARTIAL and partial is None:
        partial = route
        partial_scope = child_scope

 フルパスがマッチした場合はアスタリスクで示したrouteが呼び出されます. つまりRouteもASGIなcallableのようです.

Q. Routeがアプリケーションを持つのはなぜか?

 コルーティンチェーンはまだ終わりではなくもう一回同様の処理が続きます.

starlette/routing.py
await self.app(scope, receive, send)

 このappはrequest_response関数の内部で定義されたapp関数です. この関数のコメントによるとASGIアプリケーションのインスタンスとはrequest_response関数が返すコルーティンとして定義されているようです.

Takes a function or coroutine `func(request) -> response`,
and returns an ASGI application.

 コルーティンだった場合は高階コルーティンということになります. 個人的になぜこのappインスタンスをルーターが持っているのかがちょっと引っかかりました.

 request_responseの引数であるfuncはget_app関数が返すappコルーティンです.

 コメントの通りリクエストを受け取ってレスポンスを返すというHTTPサーバーの基本的な処理を担っているようです. 実際のリクエストを処理しているのはこのRouteがもつappで, Routeはこのオブジェクトに処理を委譲するという考え方をのようです.

 でこいつは別スレッドで実行されるようです.

raw_response = await run_in_threadpool(dependant.call, **values)

run_in_threadpoolはstarletteが提供する関数です. 内部ではrun_in_executorで処理を別スレッドに投げるようです. いわゆるワーカーというやつでしょうか. イベント・ループが進むとraw_messageには以下のようなメッセージが格納されます.

Screenshot 2019-10-03 at 9.56.13.png

結論

 Uvicornのドキュメントにあるように基本的にasync def app(scope, receive, send)というシグニチャをもつコルーティンを呼び出すわけですが実際のアプリはミドルウェアを挟んでいたり階層状になっています. この辺は一種のコールバックを入れ子にしてるのと同じ構造という感じはしますが, デバッガーは必須でしょうが処理が分割されているので読みやすいですね. 何れにしても呼び出しの一番下はルートで, そこで別スレッドでリクエストの処理を担うアプリケーションが実行されるようです.

分からなかったところ:no_good:

HttpToolsProtocolはどこで生まれるのか?

HttpToolsProtocolのイニシャライザが呼ばれるんですがそれがどこか分からなかったです.

クライアントからの要求の厳密な流れ

上の疑問ともつながりますがクライアントからリクエストが渡されるわけですが, ここの流れがふんわりとして理解できません. なんとなくブラウザを開いてデバッガーを進めるとリクエストを処理するコルーティンが実行されるという感じでスッキリしません. Serverクラスのstartupメソッドでその辺がごにょごにょされているようなんですがよく分かりませんでした.

別スレッドで実行される処理とは?

 上で示したrun_in_threadpoolに渡されるコールバックが謎です. dependantとあるのですがよく分かりませんでした.

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9