前にremote_api_shell.pyの使い方 - tagomorisのメモ置き場を書いたとき、balmysundaycandy-scala betaをリリースしました! - marblejediaryにある「トランザクションが使えない問題がこっちでは起きてない」旨の話をTwitterでもらった。が、このときは「RemoteStubはやってきたRequest/ResponseのProtocolBufferをサーバに投げてるだけだし、BeginTransaction/Commitがちゃんと投げられてれば失敗しないんじゃないの?」とか思ってた。そういやDatastoreだけRemoteDatastoreStubとして別実装になってたけど、まあ大して変わらんのじゃないの、とかスルーしつつ。(今になって思うが、するなよ……)
今日ちょっとヒマをもてあましてたので、CreateIndexの件を調べてた(結果まとめ)ときにふとremote_api_stubをだらだら読んでると……あれ? Transactionまわりの実装がなんかおかしくね? ということに気付いた。のでちゃんと読んでみた。
結論:remote_api_stub.RemoteDatastoreStubはTransactionをローカルで処理している
(2010/4/14 16:37 RemoteDatastoreStubの動作説明に処理をひとつ追記)
要するに何をやってるのか
remote_api_shellを通したときに使われるのは、大部分のサービスについてはremote_api_stub.RemoteStubで、こいつはMakeSyncCallを経由して渡ってきたrequest/responseのPB(ProtocolBuffer)の内容をほぼそのままサーバに送って処理させている。サーバ側で処理してるのは google.appengine.ext.remote_api.handler に記述されている ApiCallHandler で、またこのハンドラが受け取れるサービス/メソッドのリストは SERVICE_PB_MAP というハッシュに格納されている。
で、そこに BeginTransaction や Commit はない。(ちなみにCreateIndexもない。)
Datastoreについては完全に別の実装になっていて、概要としてはだいたい以下のような感じ。(Transaction中でない場合はどれも即座に実行されるだけなので省略)
- (クライアント側で)BeginTransaction
- このときはクライアント側でロックがとられてTransaction handelrのIdがとられるだけ
- (クライアント側で)参照アクセスを実施
- Transaction中の場合、クライアント側で変更済オブジェクトの場合はそれを参照、なければサーバにRPC
- (クライアント側で)AllocateIds/Put/Delete
- サーバに対してAllocateIdsのみ即座にRPCされる
- Putについて、IDが必要な場合はサーバにGetIDs というRPCが実行され、Put対象のEntity自体はGetIDsの結果のKeyをセットされ、ローカルのTransaction handlerに格納される
- サーバはGetIDsを受け取ったら
- BeginTransactionを実行し
- Putを実行してresponseを取得し(これがクライアントに返る)
- Rollbackする(!)
- トランザクションの衝突シミュレーションできちんとnamed keyの衝突が起きるのはこれのおかげのようだ
- Deleteについて、対象となるEntityはローカルのTransaction handlerに格納される
- (クライアント側で)Commit
- サーバに対してTransaction というRPCが実行され、Transaction handler内にあるPut/Delete内容が渡される
- サーバ側ではTransactionを受け取ったら
- BeginTransactionを実行し
- クライアント側がgetしたエンティティと現在のDatastore内のエンティティを比較し(違ってたら衝突を検出) (4/14 16:38追記)
- 渡されたPut/Delete処理を実行し
- Commitする
このあたりの処理は、以下のふたつを読み比べないといけないのでめんどくさい。妙にわかりにくいんだよな。*1
実験:RemoteDatastoreStubを(両方とも)使わなかったらどうなるか
まあSERVICE_PB_MAPに BeginTransaction/Commit がない時点で結果は明らかなんだけど。以下再現。
1. まず普通にremote_api_shellを起動
~/google_appengine$ ./remote_api_shell.py tagomoris-test /home/sigh/google_appengine/google/appengine/ext/remote_api/remote_api_stub.py:64: DeprecationWarning: the sha module is deprecated; use the hashlib module instead import sha Email: youraccount@gmail.com Password: App Engine remote_api shell Python 2.6.4 (r264:75706, Dec 7 2009, 18:45:15) [GCC 4.4.1] The db, users, urlfetch, and memcache modules are imported. tagomoris-test>
2. 最初はdatastore_v3サービス用のスタブはRemoteDatastoreStub
tagomoris-test> from google.appengine.api import apiproxy_stub_map tagomoris-test> apiproxy_stub_map.apiproxy.GetStub('datastore_v3') <google.appengine.ext.remote_api.remote_api_stub.RemoteDatastoreStub object at 0x92181cc> tagomoris-test>
3. datastore_v3サービス用のスタブをRemoteStubに置き換え
tagomoris-test> from google.appengine.ext.remote_api import remote_api_stub tagomoris-test> from google.appengine.tools import appengine_rpc tagomoris-test> server = appengine_rpc.HttpRpcServer('tagomoris-test.appspot.com', lambda: ('youraccount@gmail.com', 'yourpassword'), remote_api_stub.GetUserAgent(), remote_api_stub.GetSourceName(), debug_data=False, secure=False) tagomoris-test> stub = remote_api_stub.RemoteStub(server, '/remote_api') # 新しい RemoteStub 作成 tagomoris-test> apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub) # 既にstubが登録されているサービスに上書きしようとするとエラーに Traceback (most recent call last): File "<console>", line 1, in <module> File "/home/sigh/google_appengine/google/appengine/api/apiproxy_stub_map.py", line 232, in RegisterStub assert not self.__stub_map.has_key(service), repr(service) AssertionError: 'datastore_v3' tagomoris-test> dir(apiproxy_stub_map.apiproxy) # stubの登録内容が保持されているのはどこじゃー? ['GetPostCallHooks', 'GetPreCallHooks', 'GetStub', 'MakeSyncCall', 'RegisterStub', '_APIProxyStubMap__default_stub', '_APIProxyStubMap__postcall_hooks', '_APIProxyStubMap__precall_hooks', '_APIProxyStubMap__stub_map', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__'] tagomoris-test> del apiproxy_stub_map.apiproxy._APIProxyStubMap__stub_map['datastore_v3'] # 現在登録されているスタブを削除 tagomoris-test> apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub) # 改めて登録 tagomoris-test> apiproxy_stub_map.apiproxy.GetStub('datastore_v3') <google.appengine.ext.remote_api.remote_api_stub.RemoteStub object at 0x8cde8ac> tagomoris-test>
4. まずは普通のデータ格納のチェック
tagomoris-test> class XE(db.Model): pass ... tagomoris-test> x1 = XE() tagomoris-test> x1.put() # 成功! datastore_types.Key.from_path(u'XE', 16003L, _app_id_namespace=u'tagomoris-test') tagomoris-test>
5. そしてTransactionの実行
tagomoris-test> def tx1(): ... x2 = XE() ... x2.put() ... tagomoris-test> db.run_in_transaction(tx1) # 失敗! Traceback (most recent call last): File "<console>", line 1, in <module> File "/home/sigh/google_appengine/google/appengine/api/datastore.py", line 1904, in RunInTransaction DEFAULT_TRANSACTION_RETRIES, function, *args, **kwargs) File "/home/sigh/google_appengine/google/appengine/api/datastore.py", line 1993, in RunInTransactionCustomRetries datastore_pb.Transaction()) File "/home/sigh/google_appengine/google/appengine/api/datastore.py", line 160, in _MakeSyncCall resp = apiproxy_stub_map.MakeSyncCall(service, call, request, response) File "/home/sigh/google_appengine/google/appengine/api/apiproxy_stub_map.py", line 78, in MakeSyncCall return apiproxy.MakeSyncCall(service, call, request, response) File "/home/sigh/google_appengine/google/appengine/api/apiproxy_stub_map.py", line 278, in MakeSyncCall rpc.CheckSuccess() File "/home/sigh/google_appengine/google/appengine/api/apiproxy_rpc.py", line 111, in CheckSuccess raise self.exception CallNotFoundError tagomoris-test>
ということで、見事に失敗した。
ApiCallHandlerは本当にTransactionを処理できないのか?
さて気になるのが、ApiCallHandlerに無理矢理BeginTransaction/Commitを渡したらどうなるの? ということ。production環境内での処理ではBeginTransaction/Commitが呼ばれてるんだから、呼び出し先で実装されてないなんてことはないはず。
じゃあJava環境で失敗してるのは? HTTPリクエストをまたいだTransactionを失敗させるような何かがあるのか?
というようなことを調査すべく更にハック中。長くなったので別エントリで……。
*1:両方ともクラス名がRemoteDatastoreStubだし!