読者です 読者をやめる 読者になる 読者になる

middleware がどう動くか実装をみる

django

前回セッションのバックエンド実装について書いた。

実際に使うときは、ミドルウェアで指定する必要がある。

Djangoミドルウェアは公式ドキュメントによると object を継承したクラスをかけばいいという。自分もちょっとだけ書いたことがある。ただこのとき、固定されたメソッド名をどう拾って処理できるようにしているか、あと object を継承していてどこから定義したものがつかえるのかよくわからなかった。

参考

おさらい

設定する

MIDDLEWARE_CLASSES でタブル内に文字列でクラスのパスを指定する。

Middleware | Django documentation | Django

実装する

先ほどの SessionMiddleware の場合こうなっている

django/middleware.py at master · django/django · GitHub

Python の object を継承したクラスをつくる。あと必要なメソッドを実装すればOK。 Middleware | Django documentation | Django によると以下の5つ

  • process_request
  • process_view
  • process_exception
  • process_template_response
  • process_response

SessionMiddleware の場合、 Django 読み込み時に backend のエンジンを読み込み、 SessionStore クラスのインスタンスを保持しておく。 リクエストが来たら都度 SessionStoreインスタンスを作成、レスポンスを返すときにもろもろの処理をはさんでいる。

実装をみる

実装がどこにあるか

実装を調べようにも SessionMiddlewareMIDDLEWARE_CLASSES で読み込むよう定義しているだけ。なので、 MIDDLEWARE_CLASSES そのものを検索する。すると、 django.core.handlers.base でなにか処理していることがわかる。

django/base.py at master · django/django · GitHub

BaseHandler

BaseHandler クラスでやってることの大枠はたぶんこう

  • init
  • load_middleware
  • get_response

初期化時に process_xxxメソッドに対応する変数を初期化しておく。 継承先の __call__ で呼ばれる load_middlewareMIDDLEWARE_CLASSES で定義したミドルウェアを取り出し、ミドルウェアインスタンスの属性に process_xxx があれば追加しておく。 レスポンスを返すときにフックポイントにきたら先ほど登録したミドルウェアを取り出して実行する。

WSGIHandler

先ほどの BaseHandler クラスを使っているのが WSGIHandler__call__ されたらレスポンスを返す。どこかで while True: のようなループがあって、そこでインスタンスになっているものが待ち受けている?

django/wsgi.py at master · django/django · GitHub

使われているところを探すと、 django.core.wsgi#get_wsgi_application で使われていることがわかった。これは django.setup() して return WSGIServer するだけの関数のようだった。これは django-admin.py startproject proj したときに生成される wsgi.py で使われている。gunicorn などで動かすときに使う。

おわりに

  • 文字列で指定したクラスを import する
  • middleware から固定のメソッド名を探して格納、フック時に適用する

というような話だった。

次は余力があれば runserver か render あたりをかく。

文字列で指定しているモジュールを import する

django

前回のセッションバックエンドについて書いた。 Djangoのセッションバックエンドを調べる(file, db) - そのあれ

そのとき、 django の設定ファイルでは文字列でパスを指定している。ただ、文字列をパスで指定しているだけでどう読み込んでいるかまでよく知らなかった。

セッションバックエンドの指定では django/middleware.py at master · django/django · GitHub で使われている。

結論

__import__ をつかうか importlib.import_module をつかう。

あまり詳しく調べてないけど、特に理由がなければ importlib.import_module のほうが標準ライブラリなのでいいんじゃないかなという感じがする。実際、 import_module 関数の中ではキャッシュやロックの管理のようななにかが書かれている。

検証

リポジトリはこちら altnight/djsession at importlib · GitHub

こういう構成になっている。

$ cat app_a/__init__.py
print('init %s' % __file__)
$ cat hello.py
print('hello %s' % __file__)

__init__.pyhello.py という import すると自身のファイルパスを表示するだけのモジュール。

$ tree app_a
app_a
├── __init__.py
├── hello.py
└── level1
    ├── __init__.py
    ├── hello.py
    └── level2
        ├── __init__.py
        └── hello.py

2 directories, 6 files

ためしにつかう main.py はこういう内容。

  • import_module でモジュール指定
  • __import__ でモジュール指定
  • import_module でパッケージ指定
  • __import__ でパッケージ指定
from importlib import import_module

import_module('hello')
import_module('app_a.hello')
import_module('app_a.level1.hello')
import_module('app_a.level1.level2.hello')

#__import__('hello')
#__import__('app_a.hello')
#__import__('app_a.level1.hello')
#__import__('app_a.level1.level2.hello')

#import_module('app_a')
#import_module('app_a.level1')
#import_module('app_a.level1.level2')

#__import__('app_a')
#__import__('app_a.level1')
#__import__('app_a.level1.level2')

結果

$ python main.py
hello /Users/altnight/u/src/djsession/djsession/hello.py
init /Users/altnight/u/src/djsession/djsession/app_a/__init__.py
hello /Users/altnight/u/src/djsession/djsession/app_a/hello.py
init /Users/altnight/u/src/djsession/djsession/app_a/level1/__init__.py
hello /Users/altnight/u/src/djsession/djsession/app_a/level1/hello.py
init /Users/altnight/u/src/djsession/djsession/app_a/level1/level2/__init__.py
hello /Users/altnight/u/src/djsession/djsession/app_a/level1/level2/hello.py

ちなみにパッケージじゃなくても(=ディレクトリに init.py がなくても) import できる。

Djangoのセッションバックエンドを調べる(file, db)

django

セッションキーがどうも取得できないと思っていたら、DBじゃなくてfileバックエンドになっていたことがあった。

参考

基本的には公式ドキュメントを読むのが一番だと思う。

準備

今回試した結果のリポジトリはこれ https://github.com/altnight/djsession

pythonのインストールなど

だいたいこういうことをした

virtualenv env -p `which python3`
. ./env/bin/activate
python --version
# Python 3.4.3
pip install django
# Successfully installed django-1.8.3
pip install ipython
django-admin.py startproject djsession
django-admin.py startapp app

初期化

runserver の前に migate or syncdb が必要。今回はなんとなく admin を使いたかったので syncdb にしたけれど、どうやら syncdbdjango 1.9 で廃止されるらしい。

$ python manage.py syncdb
/path/to/djsession/virtualenv/lib/python3.4/site-packages/django/core/management/commands/syncdb.py:24: RemovedInDjango19Warning: The syncdb command will be removed in Django 1.9
  warnings.warn("The syncdb command will be removed in Django 1.9", RemovedInDjango19Warning)

Operations to perform:
  Synchronize unmigrated apps: messages, staticfiles
  Apply all migrations: contenttypes, sessions, auth, admin
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying sessions.0001_initial... OK

You have installed Django's auth system, and don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'altnight'): admin
Email address: admin@admintestsite.com
Password:
Password (again):
Superuser created successfully.

アプリの準備

  • installed app 追加
  • viewの作成
  • url 登録
  • template 作成

どこにソース上の記述があるか

だいたい django.contrib.sessions 以下をみればよさそう。今回はセッションで使っている値がどこに保存されているかを調べたかったので、 backends であっていた。

file

  • django.contrib.sessions.backends.file

db

  • django.contrib.sessions.backends.db

どこで指定するか

設定ファイルなのだけど。 django-admin.py startproject djsession で実行した時に生成された設定ファイルには記述がなかった。

Django の設定値の初期値は django.conf.global_settings にある。それをみると以下の記述があったので、DBだった。

SESSION_ENGINE = 'django.contrib.sessions.backends.db'

file

モジュールを文字列で指定しているので、 file だった。

SESSION_ENGINE = 'django.contrib.sessions.backends.file'
SESSION_FILE_PATH = os.path.join(BASE_DIR, 'session_file')

SESSION_FILE_PATH はディレクトリである必要がある。最初存在しないてきとうなパスにしたら以下のエラーが出た。

ImproperlyConfigured at /
The session storage path '/path/to/djsession/djsession/session_file' doesn't exist. Please set your SESSION_FILE_PATH setting to an existing directory in which Django can store session data.

db

SESSION_ENGINE = 'django.contrib.sessions.backends.db'

backends 以下のモジュールは以下ななので、他の方法もある。

  • (init)
  • (base)
  • cache
  • acched_db
  • signed_cookies

どこに保存されているか

file

SESSION_FILE_PATH で指定したディレクトリ以下に保存されている。

$ ls session_file
sessionidnbd3c1righob86pm4pdjuvvo8u45bud4
$cat session_file/sessionidnbd3c1righob86pm4pdjuvvo8u45bud4
Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9%

db

django_session テーブルに保存されている。

$ sqlite3 --line db.sqlite3
sqlite> .table
auth_group                  auth_user_user_permissions
auth_group_permissions      django_admin_log
auth_permission             django_content_type
auth_user                   django_migrations
auth_user_groups            django_session
sqlite> select * from django_session;
 session_key = 5lonf9nkfy1167jvlwpygz1s0dmdi6ih
session_data = MTdlZTUxZWNkMzc4NDNhODZmZGUzOGViNTdlZTc3YTA5N2Y5YjlkODp7Il9hdXRoX3VzZXJfaWQiOiIxIiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c2VyX2hhc2giOiJiN2Y0MzM0ODcxOTI2ZjQ1NGJhZTk0MjdiMmU4NzNjZWUwODEwZWZlIiwidXNlcl9pZCI6MX0=
 expire_date = 2015-08-01 10:00:19.658846

どういう内容になっているか

file

実装をみると base64エンコードされているので、デコードでOKだった。

$ ipython
In [1]: import base64
In [2]: base64.b64decode('Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9')
Out[2]: b'696ef504f370f044ced43779a7c1f76202ea20d4:{"user_id":1}'

key:value という形になっている。これの key は cookiesessionid の値。

db

こちらも base64 でデコードすればOK。

In [3]: base64.b64decode('MTdlZTUxZWNkMzc4NDNhODZmZGUzOGViNTdlZTc3YTA5N2Y5YjlkODp7Il9hdXRoX3VzZXJfaWQiOiIxIiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c    2VyX2hhc2giOiJiN2Y0MzM0ODcxOTI2ZjQ1NGJhZTk0MjdiMmU4NzNjZWUwODEwZWZlIiwidXNlcl9pZCI6MX0=')
Out[3]: b'17ee51ecd37843a86fde38eb57ee77a097f9b9d8:{"_auth_user_id":"1","_auth_user_backend":"django.contrib.auth.backends.ModelBackend","_auth_user_hash":"b7f4334871926f454bae9427b2e873cee0810efe","user_id":1}'

こちらも key:value という形になっている。なんだか value に auth_user 系のものがついてて、DBの場合挙動が違うかと思ったけど、単純に admin にログインしたままだった。

ログアウトするとこう。

In [6]: base64.b64decode('Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9')
Out[6]: b'696ef504f370f044ced43779a7c1f76202ea20d4:{"user_id":1}'

どういう実装になっているか(backend)

基本は base.py にある SessionBase を継承している。クラス名は SessionStore

以下のメソッドを実装する必要がある

  • exists
  • create
  • save
  • delete
  • load
  • clear_expired

file

os.write, shutil, tempfile あたりをつかっている。あんまり馴染みがなかったので tempfile を使ってみる。

In [1]: import tempfile

In [2]: tempfile.
tempfile.NamedTemporaryFile    tempfile.TMP_MAX               tempfile.TemporaryFile         tempfile.gettempprefix         tempfile.mkstemp               tempfile.tempdir
tempfile.SpooledTemporaryFile  tempfile.TemporaryDirectory    tempfile.gettempdir            tempfile.mkdtemp               tempfile.mktemp                tempfile.template

In [2]: tempfile.get
tempfile.gettempdir     tempfile.gettempprefix

In [2]: tempfile.gettempdir()
Out[2]: '/var/folders/fd/3vccp68n6n50prd2zj2x67mc0000gn/T'

In [3]: tempfile.gettempprefix()
Out[3]: 'tmp'

ちなみに上のほうで出てきたディレクトリを指定する必要があったという判定は、こういうコードだった。

            # Make sure the storage path is valid.
            if not os.path.isdir(storage_path):
                raise ImproperlyConfigured(
                    "The session storage path %r doesn't exist. Please set your"
                    " SESSION_FILE_PATH setting to an existing directory in which"
                    " Django can store session data." % storage_path)

db

Session というモデルを使って CRUD していた。

おわりに

fileの場合サーバーが複数台になるとそれぞれのサーバー上でセッションが保存されることになるので、リクエストしたタイミングなどによってはセッションが取得できない。そのセッションファイルはそれぞれのサーバー上の tempfile で生成される一時的なディレクトリに存在している。DBに保存しているのであれば、masterとなるDBがひとつなので、セッションファイルがわかれることはなかった。ただ、masterが複数台になるようなパターンがあったらだめなのでは。

という、一言で言えばそういう話。