Django 1.5.1 が昨日 リリースされました 。リリース内容を見るて、クエリーセットにメモリリークの問題があったそうです。

もともとのバグ

1.4 では、2回クエリーセットを解決すると、空な結果が返ってきて、前の結果がガーベージコレクションされない現象があったそうです (バグ #19895)

例えば:

qs = MyModel.objects.all()
first = list(qs)
second = list(qs) # second は first と一緒だはずなのに、空になってしまう。。

1.5 ではこの問題に対して、 修正された らしい。ただし、1.5 のリリース後にその修正はメモリリークを招いたことが分かった。

メモリリーク

この問題は Python の動きに関係があるそうです。Django の開発者は Python のバグ を登録していたんだけど、うちは社内で「それは既知の問題じゃね?」という話が出てきました。確かにこれは以前、 石本さんが指摘してくれた 問題です。恐らくこれは、Python のバグではなく、あまり知られてない仕様です。

ジェネレータの中には例外処理はダメ?

Django のクエリーセット例外処理をジェネレーターの中に行なっていた。石本さんの記事の様にジェネレーターの中に例外処理をすると、メモリが解放されないオブジェクトが出てきます。

以下の様なコードがあるとします。

class MyObj(object):
    def __iter__(self):
        self._iter = iter(self.iterator())
        return iter(self._iter)

    def iterator(self):
        try:
            while True:
                yield 1
        except Exception:
            raise

ここでは、 iterator() メソッドの中に例外処理があるので、このジェネレーターに参照があれば、ジェネレーターが使っているメモリが解放されない。

>>> import gc
>>> class MyObj(object):
...     def __iter__(self):
...         self._iter = iter(self.iterator())
...         return iter(self._iter)
...     def iterator(self):
...         try:
...             while True:
...                 yield 1
...         except Exception:
...             raise
...
>>> gc.collect()
0
>>> i = next(iter(MyObj()))
>>> gc.collect()
4
>>> gc.garbage
[<generator object iterator at 0xb722d43c>]

Django 1.5.1 の問題解決

最終的には、 1.5.1 では、 この修正を取り除く ようにして、 #19895 は未解決状態に戻った。

こういう問題は、コードを見る時に、非常に気づきづらいけど、ジェネレーターをリークしないように、気をつけるしかないでしょうねぇ。ジェネレータを出来るだけ短く、例外処理や、 with を使わないようにしたほうがいいでしょう。