たごもりすメモ

コードとかその他の話とか。

remote_apiのトランザクション処理は実はproduction環境そのものとは違った件

前に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中でない場合はどれも即座に実行されるだけなので省略)

  1. (クライアント側で)BeginTransaction
    • このときはクライアント側でロックがとられてTransaction handelrのIdがとられるだけ
  2. (クライアント側で)参照アクセスを実施
    • Transaction中の場合、クライアント側で変更済オブジェクトの場合はそれを参照、なければサーバにRPC
  3. (クライアント側で)AllocateIds/Put/Delete
    • サーバに対してAllocateIdsのみ即座にRPCされる
    • Putについて、IDが必要な場合はサーバにGetIDs というRPCが実行され、Put対象のEntity自体はGetIDsの結果のKeyをセットされ、ローカルのTransaction handlerに格納される
    • サーバはGetIDsを受け取ったら
      1. BeginTransactionを実行し
      2. Putを実行してresponseを取得し(これがクライアントに返る)
      3. Rollbackする(!)
      • トランザクションの衝突シミュレーションできちんとnamed keyの衝突が起きるのはこれのおかげのようだ
    • Deleteについて、対象となるEntityはローカルのTransaction handlerに格納される
  4. (クライアント側で)Commit
    • サーバに対してTransaction というRPCが実行され、Transaction handler内にあるPut/Delete内容が渡される
    • サーバ側ではTransactionを受け取ったら
      1. BeginTransactionを実行し
      2. クライアント側がgetしたエンティティと現在のDatastore内のエンティティを比較し(違ってたら衝突を検出) (4/14 16:38追記)
      3. 渡されたPut/Delete処理を実行し
      4. Commitする

このあたりの処理は、以下のふたつを読み比べないといけないのでめんどくさい。妙にわかりにくいんだよな。*1

  • クライアント側のコード
    • google.appengine.ext.remote_api.remote_api_stub.RemoteDatastoreStub
  • サーバ側のコード
    • google.appengine.ext.remote_api.handler.RemoteDatastoreStub

実験: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だし!