前に書いたremote_apiのトランザクション処理は実はproduction環境そのものとは違った件の続き。remote_api_shell(およびRemoteStub)経由でTransactionを処理するとどうなるのか? を調査した結果。
結論:できませんでした。
ApiCallHandlerにTransactionを渡す
前回の試行の結果、production側に何もしない状態だとSERVICE_PB_MAPにBeginTransactionなどが定義されていないので呼び出しに失敗した。
ならばSERVICE_PB_MAPにTransactionに必要なRPCメソッド情報を追加してどうなるか試してみようじゃないか、ということで以下のようなコントローラ raw_api.py を用意。
from google.appengine.api import api_base_pb from google.appengine.datastore import datastore_pb from google.appengine.ext.remote_api import handler from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app def main(): handler.SERVICE_PB_MAP['datastore_v3']['BeginTransaction'] = (datastore_pb.BeginTransactionRequest, datastore_pb.Transaction) handler.SERVICE_PB_MAP['datastore_v3']['Commit'] = (datastore_pb.Transaction, datastore_pb.CommitResponse) handler.SERVICE_PB_MAP['datastore_v3']['Rollback'] = (datastore_pb.Transaction, api_base_pb.VoidProto) application = webapp.WSGIApplication([('.*', handler.ApiCallHandler)], debug=True) run_wsgi_app(application) if __name__ == '__main__': main()
Transactionに必要なメソッド名 BeginTransaction/Commit/Rollback を、request/responseのPBクラス名とともに定義に加える。処理そのものはApiCallHandlerにそのまま渡してやればいい。
で、上記コントローラを適当なURIにマッピングする。(あとでRemoteStubからそのURIを呼び出す。ここでは '/raw_api' とした。)
application: tagomoris-test version: 2 runtime: python api_version: 1 handlers: - url: /raw_api script: raw_api.py login: admin - url: /remote_api script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py login: admin - url: /.* script: main.py
これをアップロードして、あとはもうみなさんご存じのremote_api_shell.pyから接続する。普通のput()が成功することも確認。
~/google_appengine$ ./remote_api_shell.py tagomoris-test (略) The db, users, urlfetch, and memcache modules are imported. tagomoris-test> from google.appengine.api import apiproxy_stub_map 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, '/raw_api') tagomoris-test> del apiproxy_stub_map.apiproxy._APIProxyStubMap__stub_map['datastore_v3'] tagomoris-test> apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub) tagomoris-test> class XE(db.Model): pass ... tagomoris-test> x1 = XE() tagomoris-test> x1.put() datastore_types.Key.from_path(u'XE', 27001L, _app=u'tagomoris-test') tagomoris-test>
さて、では満を持して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/.../google_appengine/google/appengine/api/datastore.py", line 2132, in RunInTransaction DEFAULT_TRANSACTION_RETRIES, function, *args, **kwargs) File "/home/.../google_appengine/google/appengine/api/datastore.py", line 2223, in RunInTransactionCustomRetries result = function(*args, **kwargs) File "<console>", line 3, in tx1 File "/home/.../google_appengine/google/appengine/ext/db/__init__.py", line 805, in put return datastore.Put(self._entity, rpc=rpc) File "/home/.../google_appengine/google/appengine/api/datastore.py", line 284, in Put raise _ToDatastoreError(err) BadRequestError: invalid handle: 3329134779050992549 tagomoris-test>
残念!
実際のところどうなのか?
AppEngineのコンソールからログを見てみると、何が起きているのか、より詳しく確認できる。順序的には次の通り。
- BeginTransactionリクエストの処理(を行うHTTPリクエスト)が成功し
- Commitリクエストが失敗し
- メッセージは "ApplicationError: 1 invalid handle: xxxxxxxxxxxxxx"
- スタックトレースを見ると、DatastoreServiceにRPCし、その結果を受け取ってチェックした結果の例外だと分かる
- Rollbackリクエストが失敗している
- エラーはCommitと同じ
スタックトレースはこんな感じ。
Exception while handling service_name: "datastore_v3" method: "Put" request < "\n\037j\032j\016tagomoris-testr\010\013\022\002XE\030\000\014\202\001\000\022\031\011-dq\025\203|\302\212\022\016tagomoris-test"> Traceback (most recent call last): File "/base/python_lib/versions/1/google/appengine/ext/remote_api/handler.py", line 309, in post response_data = self.ExecuteRequest(request) File "/base/python_lib/versions/1/google/appengine/ext/remote_api/handler.py", line 340, in ExecuteRequest response_data) File "/base/python_lib/versions/1/google/appengine/api/apiproxy_stub_map.py", line 78, in MakeSyncCall return apiproxy.MakeSyncCall(service, call, request, response) File "/base/python_lib/versions/1/google/appengine/api/apiproxy_stub_map.py", line 278, in MakeSyncCall rpc.CheckSuccess() File "/base/python_lib/versions/1/google/appengine/api/apiproxy_rpc.py", line 126, in CheckSuccess raise self.exception ApplicationError: ApplicationError: 1 invalid handle: 9998691025158235181
なぜこういう失敗となるか。原因は以下のどれかだと推測できる。前提となる理解:BeginTransaction/Commit/Rollbackがそれぞれ別のHTTPリクエストとしてproduction環境に投げられ、処理されている。
- 実はproduction環境に特有の情報が何か足りない
- 可能性はある
- が、production側で使われているPB定義(datastore_pb.py)をゴニョゴニョしたところ、PB自体の定義を行うPB(?)らしきものがあるだけで、他に追加の情報はなさそうな感じ
- えー、いやまあ可能性はあるね?
- 可能性はある
- BeginTransaction/Commit/Rollbackは実際には別のノードで処理されている
- Datastore側で、どのトランザクションハンドラがどのノードから来たものかチェックしている
- このためBeginTransactionとCommitが別々のノードから来たときに「それはおかしい」と判断できる
- これは最低限やってそう
- ちなみにTransaction試行を19回連続でやってみたが、すべてinvalid handleで失敗した
- この程度の処理でそんなに多くのノードに分散しているとは思えないので、一度くらいBeginTransactionとCommitが同じノードに割り当てられたら成功するんじゃないの?
- と思ったが全部失敗したから、まだ他にもチェック機構があるだろうな
- HTTPレスポンスを返したら、取得したトランザクションハンドルの後始末が行われている?
- レスポンスを返したあと、各ノードからDatastoreに対して「このハンドルはもういらないから破棄」というようなことをやってるのではないか
- 試行結果のトランザクションハンドル(数値)を眺めてみるとこんな感じで、数値が上下してることから、値の再利用は間違いなくやってる
- 3329134779050992549
- 3447372513697309927
- 5384575290747572498
- 17796629001013886752
- 706364654572229175
- 7448387133432909780
- 14880868972903147936
- 11879584442667429514
- 7178209122181341880
- 13880403251869159588
- 以下略
- 使用したハンドルの破棄をどのタイミングでやるかというと、一定時間おきだと処理数が爆発しそうだし、HTTPレスポンス返したらさくっと実施というのは十分に考えられる
- ていうか普通そうするような気がするね
原因の予測としてはこんな感じで、どれにせよHTTPリクエストをまたいでトランザクションを継続するのはどうも無理そう、という結論に落ち着いた。
さてどうしようか
production環境でトランザクションは動かしたい。今のところ考えられる方法は以下のふたつ。
全テストをproduction環境内で動作させる
これは既にやってる人が(多分いっぱい)いて、PythonならGAEUnitが使える。多分。まだ試してないけど大丈夫でしょう。Javaならkotoriかな。
利点は確実に何の問題もなく動作すること。欠点はちょっとコード変えるたびにデプロイしないといけない*1のと、productionのCPU時間を食うこと。特定のテストだけ実行とかはできそうだったけど。多分。
トランザクション部分だけproduction環境で関数ごとevalする
marblejenkaさんはこっちを考えてるらしい。balmysundaycandy-scalaの改良方針だと思われる。
利点は手元でちょっと書いたコードを即座に試せること。特にremote_api_shellみたいな対話環境で定義したものが即座に動くというのは実にいい。
欠点は実現が難しそうなこと。
production環境でトランザクション処理対象となる関数をevalするときには、手元で定義したばかりのクラスや作成したオブジェクトなども問題なく扱えるよう、その定義文や代入などまで含めてevalしてやる必要がある。evalするためには適当な形式でくるんで送ってやらないといけない。毎回対話環境起動後の全処理を送ってやるのも馬鹿らしいから「前のトランザクション以降のすべて」とかやりたいが、トランザクション外のDatastoreアクセス処理(put/delete等)との整合性が取れるかが心配になってくる。
あと特にpython側ではTransaction内/外の検出をPythonインタプリタの実行フレームから取得したりしてるので、このあたりの処理に不整合が起きないかも注意する必要がありそう。
うーむ
なかなか悩ましいですね。まあ「どうしてもproduction環境のTransaction処理じゃないとイヤだ!」とか言わなければ、remote_api_shell.pyから使える普通のトランザクション(RemoteDatastoreStubで処理される疑似トランザクション)でいいような気もする。他との更新衝突があれば普通に検出してくれるしね。改めて読んでみると、ちゃんと考えて作ってあるなーと思う。*2
でJavaにはそういうの無いんだっけ? RemoteDatastoreStubのクローンを作るというのはいい手だと思う、が、クライアント/サーバ両側を移植しないといかんからけっこう大変かも。うーむ。