makeAsynCallをサーバ上で実行させる

Google App Engineの各サービス(Datastore,Mail,Memcahe…)はプロダクションサーバで実行されるときにGoogleのサーバ群のどこかで実行されています。この時に使われている通信プロトコルプロトコルバッファと呼ばれるもので、バックエンドのサービスに対して何かをしたい場合に重要な部分となります。
一方、GAEの開発環境は秀逸です。本来はDatastoreなどもGoogleのサーバ上でしか動かないサービスなのですが、開発環境ではエミュレートされているため、本番サーバでなければ確認できないといった問題もありません。ただし、ローカルのエミュレート環境とサーバ上での環境は異なるため、開発環境とサーバ環境で挙動が異なるケースもあるので注意する所です。
GAEのSDKではこのような仕組みを実現するために、ApiProxyとDelegateというクラスを提供しています。Javaであれば、「com.google.apphosting.api.ApiProxy」と「com.google.apphosting.api.ApiProxy.Delegate」です。GAEのアプリケーションが何らかのサービスを実行する場合、ApiProxyのgetDelegateが呼ばれ、移譲されたクラスからサービスが実行されるわけです。tまり、ローカルの開発環境用のDelegateと本番サーバ用のDelegateが用意されていて差し替えられる設計です。
この設計をうまく利用すると「ローカル環境で実行しているアプリケーションがこっそりと本番サーバのサービスを利用する」という事が実現できます。

まず、こちらが通常の同期処理のシーケンスです。

同期処理の場合、ApplicationはmakeSyncCallというメソッドを通してサービスと通信し、byteを返す仕組みになっています。byteSDKの下位レイヤーでEntityなどのクラスにデシリアライズされます。ローカル環境ではDelegateがローカルで処理しているだけなので割愛します。

さて、これをサーバ上で利用する場合は次のようになります。

ローカル環境でのDelegateのmakeSyncCallが実行された時、Httpを利用してサーバ上のServletを呼び出します。Servletは本番環境に設置されているので直接DelegateのmakeSyncCallを呼び出し、結果のバイト配列がHttpレスポンスとして返されます。すると、ローカルのDelegateは(時間は余分にかかりますが)本番サーバで実行した結果を返すことができるわけです。
このテクニックは1年くらい前のApp Engine ja Nightで@shin1ogawaさんによって紹介されました。

Deadlineと非同期処理

最近のアップデートでDeadline(サービス実行のタイムアウト)に関してアプリケーション側で制御できるようになりました。おそらくはこの影響で、これまで同期処理(makeSyncCall)で行われていたDatastoreのqueryなどが非同期処理(makeAsyncCall)へと変更されたようです(おそらくは1.3.8から)。
一般的な開発であれば「バージョンアップは控える」という対策もとれますが、GAEは勝手にバージョンがあがるのでなんとか対応しなければなりません。この辺りはGAEのリスクの1つでしょう。

というわけで、前置き長いですが、非同期処理に対応してみたというエントリーです。まずは同じようにシーケンスを書いて確認します。

非同期処理の場合は、DelegateのmakeAsyncCallが呼ばれますが、一番の違いは戻り値の型がjava.util.concurrent.Future となっている点です。FutureはJava5の非同期処理やSwingWorkerでお馴染みのインターフェイスで、非同期処理の結果を含むコンテナで,主要メソッドはisDoneとgetです。isDoneで非同期処理の完了を確認し、getで非同期処理の結果を取り出せます。なお、getは実行するとブロッキングする性質があります(タイムアウトも指定可能)。
クライアント側のコードはこんなイメージです。

Future<byte[]> f = makeAsyncCall();
for (;;) {
   Thread.sleep(1000L); // or 別の処理
   if (f.isDone()) {
     byte[] result = f.get();
     // 結果をとれたので後処理
     break;
   }
}

通常は「他の処理を走らせながら完了を確認する」、「非同期に複数のタスクを走らせて、すべての完了をgetで待機する」といったロジックになるでしょう。

さて、HTTPを使って非同期にやろうとしても流石に無理があります。ここでは非同期処理を偽装して同期処理にしてしまう方針で設計します。Datastoreの利用などであれば、時間がかかるようになりますが非同期処理でなくても影響は少ないと判断しました。
シーケンスはこのようになります。

Applicationから見た場合makyAsynCallは非同期処理ですが、実際は同期処理としてサーバとHTTP通信を行います。サーバ側ではmakeAsycCallを使い非同期通信でサービスを利用しますが、getを使い結果が取得できるまでブロッキング(待機)して結果(byte[])を返します。
クライアント側のDelegateではFutureのインスタンスを作り返しますが、生成時点で既に処理は完了している為、実際には非同期ではありません。

実装サンプルとしては、こちらを御覧ください。同期処理用のSyncServletと非同期用のAsyncServletを用意してクライアント側からのリクエストをハンドリングしています。