App Engineでのトランザクションの分離
http://code.google.com/appengine/articles/transaction_isolation.html
はじめに
Wikipediaによるとデータベース管理システムのトランザクションの分離レベルは「ある操作による変更が並列に実行される他の操作から見えるようになるタイミングとその手段によって定義される」そうだ。この記事ではGoogle App Engineデータストアでのトランザクションの分離について説明したい。記事を読めば並列な読み書きがどのように実行されるかより深い理解が得られるだろう。
Read Committed
データベースによってサポートされる分離レベルは4つ(Serializable, Repeatable Read, Read Committed, Read Uncommitted)だが、データストアの分離レベルはそのうちのRead Committedとほとんど同じだ。データストアからクエリまたはget()で読み込まれたエンティティはコミットされたデータのみからなる。読み込まれたデータが部分的にコミットされたデータ、つまりいくつかのデータがコミット前のデータでそれ以外がコミット後、ということはありえない。ただし、クエリとトランザクションのやりとりはもう少し分かりにくい。それを理解するにはcommit()のプロセスについて細かく見る必要があるだろう。
commit()プロセス
commit()にはマイルストーンが二つある。エンティティの変更が適用されるときと、エンティティのインデックスの変更が適用されるときだ。最初のポイントをマイルストーンA、次のポイント、commit()が終了するとき、をマイルストーンBと呼ぼう。マイルストーンAに到達したときにはエンティティには全ての変更が適用されており、マイルストーンBに到達するとエンティティのインデックスの変更が適用されている。
マイルストーンAのあとで更新されたエンティティをキーを使って検索すると、エンティティの最新バージョンが得られることは保証されている。ただし、もし同時に届いたリクエストが実行するクエリの述語(SQL/GQL的に言えば`WHERE句')が更新前のエンティティに合致せず、更新後のエンティティに合致するなら、マイルストーンBに到達したcommit()操作の後だった場合だけそのエントリはリザルトセットに含まれる。つまり、 ホンの短い間、もう一度キーを使って検索してもクエリの条件を満たさないエンティティがリザルトセットに含まれることがある。クエリがどのエンティティを返すか決定する際にはマイルストーンAとマイルストーンBの間にあるアカウントの変更は見えないが、クエリが特定のエンティティを返すことを決定したあとは常にマイルストーンAバージョンのエンティティが戻されると言うことに注意しよう。
例
並列な更新とクエリがどのように相互作用するか一般的な説明をしてきたけど、私なら具体的な例を一通り眺めた方がこのコンセプトを簡単に理解できると思う。ちょっと見てみよう。単純なケースから初めて、最終的により興味深い例を提供したい。
Personエンティティを持つアプリケーションを考えよう。Personには次のような属性がある:
- 名前
- 身長
また、アプリケーションは次のような操作ができる:
- updatePerson()
- getTallPeople(), 72インチよりも背の高い人を全て返す
データストアには2つのPersonエンティティが存在する:
- アダム, 身長68インチ
- ボブ, 身長73インチ
例1 − アダムを大きくする
アプリケーションが全く同じタイミングでリクエストを二つ受け取ったとしよう。最初のリクエストではアダムの身長を68インチから74インチにする。爆発的な成長!で、二つ目のリクエストはgetTallPeople()を呼び出す。さてgetTallPeople()が返す値はどうなる?
その回答はリクエスト1によって起動される二つのcommit()マイルストーンと、リクエスト2によって実行されるgetTallPeople()クエリの関係による。次のようだとすると:
- リクエスト1、 put()
- リクエスト2、 getTallPeople()
- リクエスト1、 put()-->commit()
- リクエスト1、 put()-->commit()-->マイルストーンA
- リクエスト1、 put()-->commit()-->マイルストーンB
このシナリオだとgetTallPeople()はボブだけを返す。なぜかって?それはアダムの身長を高くするための変更がまだコミットされていなくて、リクエスト2が発行するクエリからはまだ見えないからだ。
次にこのようなものを考えてみる:
- リクエスト1、 put()
- リクエスト1、 put()-->commit()
- リクエスト1、 put()-->commit()-->マイルストーンA
- リクエスト2、 getTallPeople()
- リクエスト1、 put()-->commit()-->マイルストーンB
このシナリオではリクエスト1がマイルストーンBに届く前にクエリが実行されている。つまりPersonのインデックスへの変更はまだ適用されていない。そのため結局getTallPeople()はボブだけを返すことになる。これがクエリの条件を満たすプロパティを持つエンティティがリザルトセットに含まれない場合の例だ。
例2 − ボブを小さくする(ごめん、ボブ)
こちらの例ではリクエスト1がちょっと違う。アダムの身長を68インチから74インチに増やすのではなくボブの身長を73インチから65インチに減らす。この場合、getTallPeople()はどうなるだろう
- リクエスト1、 put()
- リクエスト2、 getTallPeople()
- リクエスト1、 put()-->commit()
- リクエスト1、 put()-->commit()-->マイルストーンA
- リクエスト1、 put()-->commit()-->マイルストーンB
このシナリオではgetTallPeople()はボブを返す。なぜか?ボブの身長を減らす更新がまだコミットされていないので、リクエスト2に発行されるクエリにはまだ変更が見えないからだ。
次にこのような例を考えよう:
- リクエスト1、 put()
- リクエスト1、 put()-->commit()
- リクエスト1、 put()-->commit()-->マイルストーンA
- リクエスト1、 put()-->commit()-->マイルストーンB
- リクエスト2、 getTallPeople()
このシナリオだとgetTallPeople()は誰も返さない。ボブの身長を減らす変更はリクエスト2のクエリが発行される時点でコミットされているからだ。
次にこのような例を考えよう:
- リクエスト1、 put()
- リクエスト1、 put()-->commit()
- リクエスト1、 put()-->commit()-->マイルストーンA
- リクエスト2、 getTallPeople()
- リクエスト1、 put()-->commit()-->マイルストーンB
このシナリオではマイルストーンBの前にクエリが実行されるので、Personのインデックスの変更はまだ適用されていない。そのためgetTallPeople()はまだボブを返すが、Personのheightプロパティは更新された値、65を返す。これがプロパティがクエリの条件を満たさないエンティティがリザルトセットに含まれる場合の例だ。
まとめ
上記の例から分かるように、Google App Engineデータストアのトランザクション分離レベルはRead Committedと極めて近い。もちろん重要な違いもあるがすでに君はその違いと理由を理解しただろう。きっと君のアプリケーションについてより賢明でデータストアに適した決定を下せるようになったはずだ。
Pythonのデコレータが面白いのでRubyで実装してみた
・・・けどいまいちだったorz
一応説明すると、デコレータって言うのはPythonにあるアノテーションでメソッドを拡張できる機能・・・らしい。
今回の目標はこれを動かすこと。(@declareArgs(float, float)がデコレータ)
http://www.itmedia.co.jp/enterprise/articles/0501/24/news034_3.html
# http://www.itmedia.co.jp/enterprise/articles/0501/24/news034_3.html import math def declareArgs(*argTypes): def checkArguments(func): assert func.func_code.co_argcount == len(argTypes) def wrapper(*args, **kwargs): pos = 1 for (arg, argType) in zip(args, argTypes): assert isinstance(arg, argType), \ "Value %r dose not match %s at %d" % (arg, argType, pos) pos += 1 return func(*args, **kwargs) wrapper.func_name = func.func_name return wrapper return checkArguments @declareArgs(float, float) def calcDistance(x, y): return math.sqrt(x * x + y * y) print calcDistance(3.14, 1.592) print calcDistance(2.0, 4)
実行結果。
# http://www.itmedia.co.jp/enterprise/articles/0501/24/news034_3.html $ /opt/Python-2.4/bin/python DecoratorTest.py 3.52052041607 Traceback (most recent call last): File "DecoratorTest.py", line 27, in ? print calcDistance(2.0, 4) File "DecoratorTest.py", line 11, in wrapper assert isinstance(arg, argType), \ AssertionError: Value 4 dose not match at 2
ていうか、なんでITmediaはデコレータの説明なのにわざわざリフレクション使いまくった例にしたんだろう。移植がめんどいよ・・・。
とりあえずデコレータはこんな感じの実装にしてみた。
# decorator.rb module Kernel def decorate(decorator, target) decorator = method(decorator) if decorator.is_a? Symbol method_name, method_body = case target when Method [target.to_s.sub(/#<Method: (\w+)#(\w+)>/, '\2'), target] else [target, nil] end if self.is_a? Class method_body ||= instance_method(target).bind(self.new) define_method method_name, &decorator.call(method_body) else method_body ||= method(target) self.class.instance_eval{define_method method_name, &decorator.call(method_body)} end end end
使ってみたところはこう。
require 'decorator' def declare_args(*arg_types) lambda do |func| raise <<-eos.strip unless func.arity == arg_types.size The number of arguments, #{func.arity}, should be #{arg_types.size}. eos lambda do |*args| args.size.times do |pos| arg, arg_type = args[pos], arg_types[pos] raise <<-eos.strip unless arg.instance_of? arg_type Value #{arg} does not match #{arg_type} at #{pos}. eos end func.call(*args) end end end def calc_distance(x, y) Math.sqrt(x * x + y * y) end decorate declare_args(Float, Float), :calc_distance puts calc_distance(3.14, 1.592) puts calc_distance(2.0, 4)
実行結果
% ruby dec1.rb 3.52052041607487 dec1.rb:12:in `calc_distance': Value 4 does not match Float at 1. (RuntimeError) from dec1.rb:10:in `times' from dec1.rb:10:in `calc_distance' from dec1.rb:27
んー、まぁ一応動いてるんだけど、アノテートみたいにメソッド定義の前に付けられないのがなぁ・・・。あとデコレータの実装見れば分かるけど、instance_method(target).bind(self.new) ←ここで新しくnewしたオブジェクトをバインドしてるのが最悪。なんとかうまく実行時のインスタンス引っ張って来れないもんか。
まぁ結局、こういうのはActiveSupportのalias_method_chainを使ったほうがいいと思った。