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
を使わないようにしたほうがいいでしょう。