LayerX エンジニアブログ

LayerX の エンジニアブログです。

AIの提案が正しそうなのに動かない理由を、uvicornのソースコードを読んで理解した話

こんにちは。LayerX Ai Workforce事業部でSWEとしてインターンをしているYuです。

本記事では、AIの提案をそのまま実装してうまくいかなかった経験や、フレームワークのソースコードを読んで解決に至ったプロセス、そしてその過程で感じたことについてお話しします。

はじめに

みなさんは、普段開発をするときに、Coding Agentを使っていますか? Claude Codeがリリースされてから、Coding Agentの流れが加速したように感じており、最近では自分自身でコードを書くこともかなり減ってきています。自分はソフトウェア開発を始めて2年ほどなのですが、自分の成長スピードを遥かに超える速度で進化するCoding Agentを見ていると、自分が学ぶ意味とは?と思ってしまうことが時々あります。 そんな中、自分なりに学ぶことの楽しさや重要性を感じたタスクがあるので、そのお話について書きたいと思います。

発生していた問題

Ai Workforce事業部では、バックエンドとしてPythonのFastAPIを使用しています。

ある時から、ローカルでの開発中にホットリロードやサーバーシャットダウンが確率的にうまくいかないという現象が生じました。同じ操作でも成功したり失敗したりするため、再現性がなく原因の特定が難しい状態でした。

本番環境での影響は特になかったのですが、開発時にホットリロードがうまくいかないというのは地味に不便で、開発生産性に影響が出ていたため、改善しようということになり自分が取り組むことになりました。

まずAIに聞いてみた

タスクに着手した直後は、SSE(Server-Sent Events)周りが原因になっていそうだなと、なんとなく予想していました。ただ、「SSEが怪しい」以上の仮説を立てられる状態ではなく、ましてやホットリロードやシャットダウンの仕組み自体を正確には理解していませんでした。

そこで、普段から使っていたコーディングエージェントに聞いてみたところ、on_event("shutdown") でSSE接続を止めるという提案をしてもらいました。

on_event("shutdown") について簡単に説明すると、FastAPIが提供する「アプリ終了時に呼ばれるフック」です。 つまり、提案された方針は、シャットダウン時にこのフックの中でフラグを立て、SSEのループを終了させるという方針です。

それっぽい方針だったので実装してみたのですが、、、うまくいきませんでした。

なぜうまくいかなかったのか。この時点の自分にはわかりませんでした。AIの提案が「もっともらしかった」からこそ、何が間違っているのかの見当がつかなかったのです。

FastAPIのシャットダウンの仕組みを理解する

AIの提案で解決できなかったことで、そもそも自分がシャットダウンの仕組みを理解していないことが問題なのではないかと思いました。 そこでまずは、ソースコードを読んで、仕組みを理解することにしました。

FastAPIは内部的には uvicorn というASGIサーバーの上で動いています。 今回の問題はシャットダウン処理に関わるものだったため、uvicorn のソースコードを読むことにしました。

コードを追っていくと、シャットダウンはおおよそ次の順番で行われていることがわかりました。

  1. SIGTERM / SIGINTの受信
  2. 新規リクエストの受付停止
  3. アクティブな接続が閉じるのを待つ(ここにSSEのような長寿命接続も含まれる)
  4. lifespanのshutdown(on_event("shutdown") など)

実際の処理も、ざっくりいうとこの順番で実行されています。

# uvicorn/server.py(簡略化・抜粋)
async def shutdown(...):
    # 新規接続の受付を停止
    server.close()
    
    # 既存の接続が終わるのを待つ
    await self._wait_tasks_to_complete()
    
    # アプリケーションのshutdown処理
    await self.lifespan.shutdown()

※より詳細な実装はこちらをご覧ください

ポイントは、lifespanのshutdownが呼ばれるのは一番最後という点です。

また、ホットリロード時の挙動についても確認しました。

uvicornのリロード機能では、サーバーはサブプロセスとして起動されており、ファイル変更が検知されるとそのプロセスに対して SIGTERM が送られます。 つまり、ホットリロードの実体は「プロセスの再起動」です。

原因の特定と、なぜAIの提案ではうまくいかなかったのか

シャットダウンの仕組みを理解した上で改めて問題を整理すると、原因は明確でした。

  1. SIGTERM / SIGINTの受信
  2. 新規リクエストの受付停止
  3. アクティブな接続が閉じるのを待つ ← SSE接続が無限ループで生き続けているため、ここで止まる
  4. lifespanのshutdown ← ここに到達しない

AIが提案した on_event("shutdown") は、ステップ4で実行される処理です。しかし、問題はステップ3で発生していました。SSE接続が閉じないからステップ4に進めない。ステップ4でSSEを止めようとしても、そもそもそこに辿り着けないのです。

また確率的にシャットダウンがうまくいかない原因もここで判明しました。SSE接続が必要な画面を開いていない場合には、SSE接続を閉じる必要がないため、問題なくステップ4に進むことができる。

つまり、問題なくシャットダウンできるかどうかは、SSE接続の有無次第なため、確率的な挙動に見えていました。

この「順番」がわかった瞬間、解決策も見えました。

ステップ4の on_event("shutdown") で対応するのではなく、もっと手前、シグナルを受け取った時点でSSE接続に終了を通知する必要があります。 具体的には、SIGTERM / SIGINT を受けたタイミングで、SSEのループが自発的に終了できるようにフラグを立てる方針にしました。ここまで解像度が上がっていると、AIに対して具体的な指示を出すことができ、一発で修正をすることができました。

実装としては、uvicornが内部で登録しているシグナルハンドラをそのまま置き換えるのではなく、その前段で少しだけ処理を挟む形にしています。 シグナルを受けたらまずSSE側に終了通知を送り、その後で元のハンドラに処理を委譲する、という流れです。 これによって、アクティブなSSE接続が終了待ちで詰まる前にループを抜けるようになり、その後の通常のシャットダウン処理や lifespan によるクリーンアップも、これまで通り正しく実行されるようになりました。

修正自体は数十行程度で、仕組みさえ理解してしまえばシンプルなものでした。

まとめ

Coding Agentはすごいスピードで進化していて、開発スタイルを大きく変えつつあります。かく言う自分自身も日々助けられており、すでにCoding Agentなしでの開発は想像ができません。

ただ今回、AIの提案をそのまま実装してうまくいかず、ソースコードを読んで解決したという経験を通じて、「原理を理解すること」の大切さを改めて実感することができました。シャットダウンの順番という、たった一つの知識があるかないかで、AIの提案が正しいかどうかの判断や、正しい解決策を導くためのAIへの指示の出し方も、全てが変わってきます。

コードを自分で書く機会は減っていくかもしれませんが、技術を理解しようとする姿勢はこれからも持ち続けたいと思います。そして幸いなことに、理解するためのハードルもAIによって下がっています。今回のuvicornのソースコードも、基本的にはAIと並走しながら理解を進めました。

また、シンプルに自分の知らないことを理解するというのは非常に楽しいことだという、初心を思い出すこともできました。

AIなどのツールをうまく活用して、自分自身の成長に繋げていきたいです。

おわりに

LayerXでは「Bet AI」を掲げ、全社でAIの活用と新しい開発スタイルの模索を推進しています。圧倒的なスピードで進化するAIを楽しみながら、自らの技術力もアップデートし続けたいエンジニアインターンを絶賛募集中です!

▼ 技術を楽しみながら確かな成長を目指すエンジニアインターンに興味のある方はこちら

open.talentio.com

▼ 「Bet AI」を掲げるエンジニア組織の裏側をもっと知りたい方はこちら

speakerdeck.com

技術への知的好奇心を絶やさず、AIと共に新しい開発の形を模索したい方、まずはカジュアルにお話しできるのを楽しみにしています!