たごもりすメモ

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

remote_api_shell.pyの使い方

手元の端末からGAE production環境のデータストアに接続する

GAE/pythonSDKについているDatastoreモドキ(datastore_file_stub)は実に機能が貧弱で、トランザクションの衝突を手元でシミュレートしたりできない。これは本物のDatastoreの理解には割と困る。
で、本番環境のDatastoreに手元のコンソール(Pythonの対話実行環境)から繋ぎたいよね、と誰しも思うはずだ。思うよね。

この方法はGAE Python SDKに用意されている。自分で作ってみようと思うと色々面白いけどけっこう大変だし完璧に車輪の再生産なのであまりやらない方がいい。(と思う。自分は後悔した。)
ただしWebのドキュメントには使い方がないし、SDK内にもない。該当のスクリプトのコメントを見ればわかるがそれもアレなので、ここに一応書いておく。ていうか「remote_api_shell 使い方」でぐぐって4件て。GAE Pythonてそんなに人気ないの。(笑)

remote_api_shell.py とは

pythonの対話環境(引数なしでpythonを実行すると動くやつ)で、本番のGAE production環境に接続できるようにしましょう、というやつ。SDKに入ってます(remote_api_shell.py)。でもそれ読んでもいまいち使いかたがわかりません。
動作させるには、以下の前提が必要。

  • application_id が取得済みであること
  • production環境になんらかのコードがデプロイされていること
    • そしてその app.yaml に、remote_api 関連の設定が追加済みであること

2点目が必要です。無いと動きません。最初にコードをデプロイする前にDatastoreの理解を完璧にするぜ! とか無理です。(自分はこれでハマってた。)

production環境側の準備

チュートリアルで一度でもproduction環境へのコードのデプロイが行われていれば、あとは実は特に難しくありません。

  • app.yamlへの変更
    • handlersに以下の内容を追加
handlers:
- url: /remote_api
  script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
  login: admin
    • このあたりの説明はSDKgoogle/appengine/ext/remote_api/handler.py ファイル先頭のコメントを見るといくらか書いてある
      • ていうかそのファイルにたどり着くまでが長かったよ! orz
    • アプリケーション本体で user_service 使うつもりがなくても、ハンドラが別なら特に干渉するわけでもないのでここでは素直に login:admin は加えておきましょう
  • production環境へのデプロイ
    • 素直に以下のコマンドで
 $ ./appcfg.py update path-to-project-dir/

これで完了。試しにブラウザで http://hogehoge.appspot.com/remote_api にアクセスしてみると、Googleアカウントでの認証を求められた上で "This request did not contain a necessary header" とか言われるはず。

remote_api_shell を使う

準備ができたら手元で起動する。対話環境内でproduction環境のDatastore(望めばその他のサービスも)にアクセスできているのがわかるはず。

$ ./remote_api_shell.py application_id
/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.
application_id> 

あとはこの環境内で好きにコードが書けるはず。2端末使ってこんなふうにすれば、トランザクションの衝突もちゃんと確認できる。
まずひとつめの端末で以下のように実行。

app> class XE(db.Model):
...   p = db.StringProperty()
... 
app> def tx1():
...   x = XE.get_by_key_name("xxx")
...   if x is not None: return x
...   import time
...   time.sleep(30)
...   x = XE(key_name="xxx", p="x1")
...   x.put()
...   return x
... 
app> x1 = db.run_in_transaction(tx1)

実行したら途中(sleepのところ)で30秒固まるので、そのうちにもう一方の端末で以下のコードを実行。(実際には put() 前までやっておいて、最後に put() だけ実行すればいい)

app> class XE(db.Model):
...   p = db.StringProperty()
... 
app> x2 = XE(key_name="xxx", p="x2")
app> x2.put()
datastore_types.Key.from_path(u'XE', u'xxx', _app_id_namespace=u'application_id')
app> 

最後まで成功したら前の端末に戻ってみると、sleepが完了した時点で以下のようになる。

app> x1 = db.run_in_transaction(tx1)
WARNING:root:Transaction collision for entity group with key datastore_types.Key.from_path(u'XE', u'xxx', _app_id_namespace=u'application_id'). Retrying...
app> x1
<__main__.XE object at 0xa0f0b0c>
app> x1.p
u'x2'
app> 

これでトランザクションが一度衝突し、リトライがかかった結果、他方でput()済みのオブジェクトが返ってきている、ということがわかる。

ああ長かった……。