第1章 入口と起動:get_wsgi_application / WSGIHandler / django.setup

Vol.1 では、TCP ソケットから HTTP を組み立て、WSGI と ASGI という2つのインタフェース仕様を読み解き、最小の WSGI フレームワークと最小の ASGI アプリケーションを自分の手で書きました。 application(environ, start_response) という callable が、サーバとフレームワークを疎結合につなぐ約束ごとだということも確かめました。

本書(Vol.2)では、その約束ごとの上で Django、Starlette、FastAPI が実際に何をしているのかを、フレームワークの実物のソースコードを読みながら追いかけます。 最初の題材は Django です。 runserver でも Gunicorn でも、Django は Vol.1 で書いた application(environ, start_response)まったく同じ形で WSGI サーバにつながります。では、その callable はいつ、何を初期化して生まれ、1リクエストごとに何を回しているのでしょうか。 この章では、その「起動の中身」と「応答側の最適化」まで、ソースで開いていきます。

Tip

ここでは Django の実際のソースコードを引用します。 引用ブロックの先頭には # django/core/handlers/wsgi.py Django 5.2 のように出典を記します。 省略は # ...(中略)... で示します。 手元で同じコードを開いて照らし合わせたい場合は、付録A の手順でソースを用意してください。 本書の基準バージョンは Django 5.2.15(LTS)です。

この章で学ぶことは次のとおりです。

  • wsgi.py が Django と WSGI サーバをどうつなぐか

  • get_wsgi_application()なぜ薄いラッパなのか

  • django.setup() が起動時に何を初期化するか(apps.populate() の3フェーズ)

  • WSGI callable の本体 WSGIHandler.__call__ が、リクエストの一生をどう回すか

  • 応答側の最適化 wsgi.file_wrapper が何を肩代わりするか

  • environ 辞書がどこで Django の request オブジェクトに渡されるか

1.1 wsgi.py:Django と WSGI サーバの接点

Django プロジェクトを django-admin startproject myproject で生成すると、myproject/wsgi.py というファイルが自動的に作られます。

# myproject/wsgi.py(startproject が自動生成するコード)
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

application = get_wsgi_application()

このファイルはわずか数行ですが、WSGI サーバと Django を接続する唯一の入り口です。 Gunicorn で Django を起動するとき gunicorn myproject.wsgi:application と指定しますが、これは「myproject/wsgi.py モジュールの中にある application という名前の WSGI callable を使え」という意味でした(Vol.1「WSGI が生まれた背景」を参照)。

wsgi.py が担っている責務は次の二つです。

  • 設定モジュールの宣言: DJANGO_SETTINGS_MODULE 環境変数を設定し、Django がどの設定ファイルを読み込むべきかを宣言します。この環境変数が未設定のまま Django を起動すると ImproperlyConfigured 例外が発生します。

  • WSGI callable の生成: get_wsgi_application() を呼び出して WSGI callable を生成し、モジュールレベル変数 application に束縛します。

setdefault を使っているのは、すでに環境変数として設定済みなら上書きしないためです。 コンテナや systemd など、外側で DJANGO_SETTINGS_MODULE を渡す運用にも対応できます。

1.2 get_wsgi_application:なぜ薄いラッパか

application の実体を返す get_wsgi_application() は、Django のソースでは驚くほど短い関数です。

 1# django/core/wsgi.py — Django 5.2
 2import django
 3from django.core.handlers.wsgi import WSGIHandler
 4
 5
 6def get_wsgi_application():
 7    """
 8    The public interface to Django's WSGI support. Return a WSGI callable.
 9
10    Avoids making django.core.handlers.WSGIHandler a public API, in case the
11    internal WSGI implementation changes or moves in the future.
12    """
13    django.setup(set_prefix=False)
14    return WSGIHandler()

やっていることは二つだけです。

  1. django.setup(set_prefix=False) で Django を初期化する。

  2. WSGIHandler() のインスタンスを生成して返す。

docstring が明言しているとおり、この関数は WSGIHandler を公開 API にしないための薄いラッパです。 “in case the internal WSGI implementation changes or moves in the future”。将来 WSGI 実装が変わっても利用者のコードを壊さないよう、入り口を get_wsgi_application() という名前に固定しているわけです。 wsgi.pyfrom django.core.handlers.wsgi import WSGIHandler と書かずに済むのは、この一段のラッパがあるからです。 「なぜこの関数がわざわざ存在するのか」がソースの docstring から分かる。これが実物を読む価値です。

引数の set_prefix=False にも理由があります。 setup() は既定ではスクリプトプレフィックス(URL の前置パス)を設定しますが、WSGI 経由ではこの時点では設定しません。 プレフィックスはリクエストごとに WSGIHandler.__call__ の中で設定されるからです(後述)。 FORCE_SCRIPT_NAME の設定やサブパスへのマウントに対応するため、プレフィックスの確定をリクエスト単位まで遅らせているのです。

1.3 django.setup が起動時にやること

django.setup() を「初期化処理」の一語で済ませず、ソースを開きましょう。 これは Django プロセスの起動時に一度だけ走る、最も重要な前払い処理です。

 1# django/__init__.py — Django 5.2
 2def setup(set_prefix=True):
 3    """
 4    Configure the settings (this happens as a side effect of accessing the
 5    first setting), configure logging and populate the app registry.
 6    Set the thread-local urlresolvers script prefix if `set_prefix` is True.
 7    """
 8    from django.apps import apps
 9    from django.conf import settings
10    from django.urls import set_script_prefix
11    from django.utils.log import configure_logging
12
13    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
14    if set_prefix:
15        set_script_prefix(
16            "/" if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
17        )
18    apps.populate(settings.INSTALLED_APPS)

setup() の本体は三つです。 ログ設定(configure_logging)、スクリプトプレフィックスの設定(set_prefix=True のときだけ)、そして本命の apps.populate(settings.INSTALLED_APPS) です。 get_wsgi_application()set_prefix=False で呼んでいたので、WSGI 経由ではログ設定とアプリレジストリの構築だけが走ります。

1.3.1 apps.populate:アプリレジストリを組み立てる3フェーズ

apps.populate() は、INSTALLED_APPS に並べたアプリを読み込み、モデルを登録し、各アプリの起動フックを呼ぶ。Django が「使える状態」になるための中核です。

 1# django/apps/registry.py — Django 5.2(Apps.populate)
 2def populate(self, installed_apps=None):
 3    """
 4    Load application configurations and models.
 5
 6    Import each application module and then each model module.
 7
 8    It is thread-safe and idempotent, but not reentrant.
 9    """
10    if self.ready:
11        return
12
13    with self._lock:
14        if self.ready:
15            return
16        if self.loading:
17            # Prevent reentrant calls to avoid running AppConfig.ready()
18            # methods twice.
19            raise RuntimeError("populate() isn't reentrant")
20        self.loading = True
21
22        # Phase 1: initialize app configs and import app modules.
23        for entry in installed_apps:
24            if isinstance(entry, AppConfig):
25                app_config = entry
26            else:
27                app_config = AppConfig.create(entry)
28            # ...(中略:ラベル重複チェック)...
29            self.app_configs[app_config.label] = app_config
30            app_config.apps = self
31        # ...(中略:アプリ名の重複チェック)...
32        self.apps_ready = True
33
34        # Phase 2: import models modules.
35        for app_config in self.app_configs.values():
36            app_config.import_models()
37        self.clear_cache()
38        self.models_ready = True
39
40        # Phase 3: run ready() methods of app configs.
41        for app_config in self.get_app_configs():
42            app_config.ready()
43
44        self.ready = True
45        self.ready_event.set()

3つのフェーズがはっきり分かれています。

  1. Phase 1(アプリ設定の初期化): INSTALLED_APPS の各エントリから AppConfig を生成し(AppConfig.create)、アプリモジュールをインポートします。ラベルやアプリ名の重複はここで ImproperlyConfigured として弾かれます。

  2. Phase 2(モデルのインポート): 各アプリの import_models() でモデルモジュールを読み込み、モデルクラスをレジストリに登録します。models_ready が立つまで、モデルの参照は安全ではありません。

  3. Phase 3(ready() の実行): 各 AppConfig.ready() を呼びます。シグナルの接続やシステムチェックの登録など、「全モデルが出そろった後でやりたい起動処理」をアプリがここに書きます。

フェーズを分けているのは、初期化に順序があるからです。 「アプリを全部認識してからモデルを読む」「モデルを全部読んでから ready() を呼ぶ」という段取りがあるからこそ、ready() の中で他アプリのモデルを安全に参照できます。 かつての Django は「モデルのインポートの副作用」に初期化を依存させていましたが、明示的なアプリレジストリ(AppConfigapps.populate)へ刷新され、この順序が保証されるようになりました。

../_images/fig-d1-apps-populate.jpg

図1-1 apps.populate の3フェーズ

Phase 1(apps_ready)→ Phase 2(models_ready)→ Phase 3(ready())の順に初期化が進み、各フェーズの完了でフラグが立ちます。この順序保証によって、Phase 3 で他アプリのモデルを安全に参照できます。

注釈

docstring の “It is thread-safe and idempotent, but not reentrant” は実装にそのまま現れています。

  • idempotent(冪等): 冒頭の if self.ready: return で、二度目以降の呼び出しは即座に戻ります。setup() が複数経路から呼ばれても初期化は一度きりです。

  • thread-safe(スレッド安全): with self._lock: で囲み、WSGI callable を初期化する前にスレッドを作るサーバでも二重初期化しません。

  • not reentrant(再入不可): self.loading 中に再び populate() が呼ばれると RuntimeError("populate() isn't reentrant")ready() の中からモデルのインポートを誘発して populate() を再入させる、といった事故を防ぎます。

重要

populate() はプロセスの生存期間中に一度だけ完了します。 Gunicorn のワーカーが fork される前にこの処理が走るため、各ワーカーは初期化済みの状態で起動します。 裏を返せば、設定やモデル定義の変更はプロセスを再起動しないと反映されません。「settings.py を変えたのに効かない」の多くは、ワーカーの再起動忘れです。

この起動シーケンスを図にすると次のようになります。

wsgi.pyapplication = get_wsgi_application()get_wsgi_application()apps.populate(INSTALLED_APPS)django.setup(set_prefix=False)WSGIHandler()Phase 1AppConfig 生成・app importPhase 2import_models()Phase 3AppConfig.ready() configure_logging 後ready 完了

図1-2 get_wsgi_application から apps.populate までの起動シーケンス

1.4 WSGIHandler の骨格

get_wsgi_application() が返す WSGIHandler のインスタンスこそが、Django の WSGI アプリケーション本体です。

1# django/core/handlers/wsgi.py — Django 5.2
2class WSGIHandler(base.BaseHandler):
3    request_class = WSGIRequest
4
5    def __init__(self, *args, **kwargs):
6        super().__init__(*args, **kwargs)
7        self.load_middleware()

短いですが、重要な情報が二つ詰まっています。

  • request_class = WSGIRequest: このハンドラがリクエストを表現するのに使うクラスです。environ 辞書を受け取って Django の HttpRequest に仕立てます(本章末で受け渡しだけ確認し、解剖は次章(第2章)「HttpRequest と HttpResponse の解剖」)。

  • __init__self.load_middleware() を呼ぶ: 親クラス BaseHandlerload_middleware() を呼び、settings.MIDDLEWARE からミドルウェアチェーンを構築します。これはインスタンス生成時に一度だけ実行されます。つまりミドルウェアの連結はリクエストごとではなく、サーバ起動時に済ませてあるのです。

WSGIHandler が継承している BaseHandler は、WSGI と ASGI に共通する処理を集めた基底クラスです。

 1# django/core/handlers/base.py — Django 5.2
 2class BaseHandler:
 3    _view_middleware = None
 4    _template_response_middleware = None
 5    _exception_middleware = None
 6    _middleware_chain = None
 7
 8    def load_middleware(self, is_async=False):
 9        ...
10
11    def get_response(self, request):
12        ...

load_middleware() がミドルウェアを連結して _middleware_chain を作り、get_response() がそのチェーンを実行します。 この二つの中身は第4章「ミドルウェアチェーンの構築と実行」で詳しく追います。 ここでは「__init__ がチェーンを組み立て、__call__ がそれを呼ぶ」という骨格だけ押さえておきましょう。 django.setup()(アプリ初期化)も load_middleware()(チェーン構築)も、起動時に前払いし、リクエスト時は使い回す。この設計は本書を通して何度も再会します。

図1-3は、その「前払い」と「使い回し」の対応関係を整理したものです。 左列が起動時に一度だけ走る前払い、右列がリクエストごとに使う処理で、各前払いが対応する使い回し先へつながっています。

起動時に一度だけ(前払い)リクエストごと(使い回す)django.setup()apps.populate でアプリ初期化load_middleware()ミドルウェアチェーンを構築WSGIHandler()WSGI callable を生成WSGIHandler.__call__1 リクエストを処理get_response()構築済みチェーンを実行アプリレジストリ参照モデル・設定を使う 生成済みを呼ぶ構築済みチェーンを回す初期化済みを参照

図1-3 起動時に前払いした準備を、リクエストごとに使い回す対応関係

1.5 __call__ がリクエストの一生を回す

WSGI callable の本体は __call__ メソッドです。 Vol.1 で関数として書いた application(environ, start_response) を、Django はクラスの __call__ として実装しています。

 1# django/core/handlers/wsgi.py — Django 5.2
 2def __call__(self, environ, start_response):
 3    set_script_prefix(get_script_name(environ))
 4    signals.request_started.send(sender=self.__class__, environ=environ)
 5    request = self.request_class(environ)
 6    response = self.get_response(request)
 7
 8    response._handler_class = self.__class__
 9
10    status = "%d %s" % (response.status_code, response.reason_phrase)
11    response_headers = [
12        *response.items(),
13        *(("Set-Cookie", c.output(header="")) for c in response.cookies.values()),
14    ]
15    start_response(status, response_headers)
16    if getattr(response, "file_to_stream", None) is not None and environ.get(
17        "wsgi.file_wrapper"
18    ):
19        response = environ["wsgi.file_wrapper"](
20            response.file_to_stream, response.block_size
21        )
22    return response

リクエストが届くたびに、この __call__ が一回呼ばれます。 処理を順に読み解きましょう。

  1. set_script_prefix(get_script_name(environ)): リクエストごとにスクリプトプレフィックスを設定します。get_wsgi_application()set_prefix=False にしていたのは、ここでリクエスト単位に設定するためです。

  2. signals.request_started.send(...): 「リクエスト処理が始まった」というシグナルを送ります(WHY は後述)。

  3. request = self.request_class(environ): environ 辞書を WSGIRequest オブジェクトに変換します。生の辞書アクセスが request.methodrequest.path といった属性アクセスに化けるのはここです。

  4. response = self.get_response(request): ミドルウェアチェーンと URL 解決とビューを通過させ、HttpResponse を得ます。Django の機能の大半はこの一行の内側にあります(第3章「URL 解決の全行程」、第4章「ミドルウェアチェーンの構築と実行」、第5章「ビューの呼ばれ方」)。

  5. ステータスとヘッダーの組み立て: response.status_coderesponse.reason_phrase から "200 OK" のようなステータス行を作り、ヘッダーのリストを組み立てます。ここで Set-Cookie ヘッダーをクッキーの数だけ個別に展開しているのが目を引きます。Vol.1 で「ヘッダーは名前が重複しうる」と学んだことの実例です。

  6. start_response(status, response_headers): WSGI の約束ごとに従い、ステータスとヘッダーをサーバへ渡します。

  7. wsgi.file_wrapper の分岐: レスポンスがファイル送信なら、サーバが提供する wsgi.file_wrapper を使って効率的に送ります(次節で開きます)。

  8. return response: バイト列のイテラブルを返します。

1.5.1 request_started シグナルは何のためか

2番目のステップ signals.request_started.send(...) は、Vol.1 で自作したサーバには無かった仕掛けです。 何がこのシグナルを待ち受けているのか、django.db のソースに答えがあります。

 1# django/db/__init__.py — Django 5.2
 2def reset_queries(**kwargs):
 3    for conn in connections.all(initialized_only=True):
 4        conn.queries_log.clear()
 5
 6
 7signals.request_started.connect(reset_queries)
 8
 9
10def close_old_connections(**kwargs):
11    for conn in connections.all(initialized_only=True):
12        conn.close_if_unusable_or_obsolete()
13
14
15signals.request_started.connect(close_old_connections)
16signals.request_finished.connect(close_old_connections)

request_started には reset_queries(クエリログのクリア)close_old_connections(古い DB 接続の回収) が接続されています。 close_old_connectionsrequest_finished(リクエスト終了)にも接続されています。 つまり Django は「リクエストの境界で DB 接続を点検し、寿命切れの接続を捨てる」段取りを、シグナル経由で組み込んでいるのです。 CONN_MAX_AGE で接続を使い回す運用でも、古くなった接続がリクエストをまたいで腐らないのはこの仕掛けのおかげです。 自作サーバに無かった「フレームワークが面倒を見てくれているもの」の一例です。

この __call__ の流れを図にすると次のようになります。

WSGI サーバ(Gunicorn など)WSGIHandler.__call__(environ, start_response)① set_script_prefix② request_startedreset_queries / close_old_connections③ WSGIRequest(environ)④ get_response(request)ミドルウェア→URL解決→ビュー⑤ status / headers 組み立てSet-Cookie を個別展開⑥ start_response⑦ wsgi.file_wrapper 分岐 environ, start_responseバイト列イテラブルを return

図1-4 WSGIHandler.__call__ の処理シーケンス

1.6 wsgi.file_wrapper:応答側の最適化

__call__ の末尾の分岐を、もう少し開きましょう。

1# django/core/handlers/wsgi.py — Django 5.2(__call__ 末尾の再掲)
2if getattr(response, "file_to_stream", None) is not None and environ.get(
3    "wsgi.file_wrapper"
4):
5    response = environ["wsgi.file_wrapper"](
6        response.file_to_stream, response.block_size
7    )
8return response

この分岐が成立する条件は二つです。

  • response.file_to_streamNone でない: レスポンスがファイルを送るもの(FileResponse)であること。FileResponse は送信対象のファイルオブジェクトを file_to_stream 属性に持ちます。

  • environ["wsgi.file_wrapper"] が存在する: WSGI サーバが wsgi.file_wrapper を提供していること。

両方そろうと、Django は response.file_to_stream(ファイル)と response.block_size(読み出しブロックサイズ)を wsgi.file_wrapper に渡し、その戻り値をレスポンスとして返します。

なぜわざわざサーバの機能に委ねるのでしょうか。 wsgi.file_wrapper は PEP 3333(WSGI 仕様)がサーバ側に求めるオプション機能で、「ファイルを送るならサーバの効率的な手段を使ってよい」という約束ごとです。 多くのサーバは内部で sendfile(2) のようなゼロコピー送信(カーネル内でファイルからソケットへ直接転送し、アプリのメモリを経由しない)を使います。 Django がバイト列を一塊ずつ read() してイテレートするより、サーバに丸投げした方が速いのです。 「アプリは何を送るかだけ決め、どう送るかはサーバに任せる」。WSGI の疎結合が応答側にも効いている好例です。

注釈

wsgi.file_wrapper が無いサーバ(提供は任意)でも壊れません。 その場合 FileResponse 自身がイテラブルとして振る舞い、block_size ごとに読み出して送ります。 「あれば使う、無ければ自前で送る」という安全な分岐になっています。

1.7 environ はどこで request になるのか

__call__ の3番目のステップ self.request_class(environ) を、受け渡しの部分だけ覗いておきましょう。 request_classWSGIRequest でした。

 1# django/core/handlers/wsgi.py — Django 5.2
 2class WSGIRequest(HttpRequest):
 3    def __init__(self, environ):
 4        script_name = get_script_name(environ)
 5        path_info = get_path_info(environ) or "/"
 6        self.environ = environ
 7        self.path_info = path_info
 8        self.path = "%s/%s" % (script_name.rstrip("/"), path_info.replace("/", "", 1))
 9        self.META = environ
10        # ...(中略:content_type の解析・本文ストリームの準備)...
11        self.method = environ["REQUEST_METHOD"].upper()

ここで起きているのは、Vol.1 で見た environ 辞書の各キーを、Django が使いやすい属性へ写し替える作業です。

  • environ["REQUEST_METHOD"]request.method

  • パス情報 → request.pathrequest.path_info

  • environ 辞書そのもの → request.META