2009年11月7日土曜日

#appengine の「データストアの読み込み専用状態」をエミュレートする

先日もメンテナンスがあり、AppEngineのデータストアが読み込み専用状態になってましたね。データストアへの書き込みを行うアプリケーションではこの「読み込み専用状態」時の対策をする必要があるのですが、「読み込み専用状態」の時の振る舞いをテストする方法を書いておきます。自動テストができる範囲は広ければ広い程よいですもんね。

ApiProxy.Delegateを実装・適用するだけ

ApiProxy.Delegateを実装して、makeSyncCall()メソッド内で「Datastoreへの書き込み」の時に ApiProxy.CapabilityDisabledException( )を投げてやるだけ。私は下記のようなユーティリティを作ってテストしてます。

static <R>R runOnReadOnlyMode(Callable<R> callable) throws Exception {
  @SuppressWarnings("unchecked")
  final ApiProxy.Delegate<Environment> backupedDelegate = ApiProxy.getDelegate();
  Delegate<Environment> delegate = new Delegate<Environment>() {

    public void log(Environment arg0, LogRecord arg1) {
      backupedDelegate.log(arg0, arg1);
    }

    public byte[] makeSyncCall(Environment arg0, String service, String method, byte[] arg3)
        throws ApiProxyException {
      if (service.equalsIgnoreCase("datastore_v3")
          && (method.equalsIgnoreCase("Put") || method.equalsIgnoreCase("Delete"))) {
        throw new ApiProxy.CapabilityDisabledException(service, method);
      }
      return backupedDelegate.makeSyncCall(arg0, service, method, arg3);
    }
  };
  ApiProxy.setDelegate(delegate);
  try {
    return callable.call();
  } finally {
    ApiProxy.setDelegate(backupedDelegate);
  }
}

こんなカンジにできるので、他にもDatastoreTimeoutExceptionやらApiProxyException系の例外、例えばApiProxy.UnknownExceptionなんかが発生したときの振る舞いも簡単に自動試験できますね!

不思議

Datastoreについては上記の方法で簡単にApiProxy.CapabilityDisabledExceptionをエミュレートできるのですが、MemcacheServiceについては簡単には行かない事が判明しています。

  • MemcacheServiceは例外をキャッチした際に、それをログ出力するだけのハンドラでExceptionを握りつぶす実装になっている
  • 先日の読み込み専用モードの時にログが記録されていない気がする=Memcacheサービスは読み込み専用状態に書き込み要求を受けても例外をスローしない…?

前者についてはMemcacheService#setErrorHandler()メソッドに、new com.google.appengine.api.memcache.StrictErrorHandler()を設定してやればちゃんと例外が飛ぶのでまぁなんとかなるのですが、後者については次回のメンテナンスの時になるまで調査ができませんね。ひょっとするとMemcacheサービスの仕様なのかな?という気もします。「常にMemcacheから値を取得できるとは限らない」という前提なので、書き込みに失敗しても問題ない、という仕様なのかな?それであれば、そもそも試験する必要がありませんね。
どなたか前回のメンテナンス時にMemcacheサービスから書き込みエラーとか何かの例外を受け取った人がおられたら、わかる範囲で良いので教えてください!

読み込み専用モードを考慮してアプリ側で工夫できる事もある

がっつりデータストアに書き込む必要がある処理はどうしよーも無いのですが、例えばカウンタの記録など「書き込みがメインでは無いが、書き込み処理も付随する」ような機能はちょっと工夫しておけば読み込み専用時にも稼働させる事ができます。

私が稼働させているAppEngineアプリのひとつに空うさぎというFriendFeed/Twitter/RSSクライアントアプリケーションのファイル管理用の小さいアプリケーションがあるのですが、このアプリの中にファイルのダウンロード件数を記録する、という機能があります。この機能は「ファイルのイメージをレスポンスする」と同時に「ダウンロード数カウンタを増加させるTaskをQueueに投入する」という実装をしています。投入されたTaskはデータストアの書き込みに成功しない場合はHttpStatusの500を返すように実装しているので、読み込み専用時は常に500を返す事になります。その結果、読み込み専用モードが解除されるまではAppEngineのTaskQueueに残り続けるので、読み込み専用モードが解除された頃にはカウンタの整合性はいずれ正しくなる、という動作をします。AppEngineはまだベータバージョンですし、工夫でしのげる場所はうまく実装しておきたいですね。

2009年11月4日水曜日

#appengine でスキーマ変更に対応するバッチ処理を行う

2009/11/05追記

ひがさんより指摘を頂いて、30秒制限に関する補足を本文中に青字で追記しました。いつもありがとうございます、助かります>ひがさん

ここから本文

タイトルの処理について、いくつかノウハウを書いておきます。ポイントは以下の2点。

  • 全てのエンティティにスキーマバージョンを保持する
  • ローカル環境からデプロイ環境へ直結してバッチ処理を実行する事で、30秒制限なんて無視してしまう

実例をもとに説明してみます。最近、appengine java night用のまとめページとかに使おうとしているサイトを運営していて、そこに「TwitterでAppEngine関連についてつぶやかれた内容を収集する」という機能を実装しました。しかし、つぶやきを保存する際の投稿者の情報として「Name」を保持しているものの「ScreenName」を保持しておらず、投稿者のタイムラインページへのリンクを作成できないという問題がおこっていました。なので、つぶやき保存用のエンティティに「ScreenName」という属性を新たに追加し、つぶやきの保存時にはその値を取得するように修正しました。しかし、この修正以前に保存されたつぶやきについては、ScreenName属性を持たない状態になってしまっているので、その値を設定してやる必要があります。今回はこれをバッチ処理しようと思います。
あ、この問題は、バッチ処理の説明(このエントリ)を書くための布石であって、不具合では無いんですよっっ?

全てのエンティティにスキーマバージョンを保持する

主に今回の修正のような「属性の追加」が危険なのです。AppEngineでは「特定の属性を持たないエンティティ」というスキャンができません。インデックスを定義していても、そのインデックス定義に関する属性を持たないエンティティはそもそもインデックスのエントリが作成されないからです。そこで、エンティティの定義にスキーマバージョンを最初から用意しておく事をおすすめします。今回の修正だと、エンティティ用のクラスの修正は以下のようになっています。

ローカル環境からデプロイ環境へ直結してバッチ処理を実行

移行処理をしてやるエンティティの抽出は上に書いた「スキーマバージョン」の存在により、簡単に抽出できるようになりましたので、移行処理の準備は問題ありません。
が、移行処理自体はどのような手段を使うのか?が問題です。AppEngineでは1リクエスト30秒の制限があるので、一度に大量のデータは処理できません。

  • 特定の件数で制限してcronする?
  • 特定の件数ずつばらしてtaskqueueを使う?
まぁ、どちらでも可能です。でも、本来動作させたいプロダクトコードとは関係ないモジュールをサーバにデプロイするのもあんまり気持ちよいものではありません。そこでローカル環境からバッチ処理を行おう、というのが今回の趣旨です。データストアに対する一回の操作(Datastore#put()とかDatastore#get()とか、データストアサービスに対する一回のメソッド実行)に関しては30秒制限がある事は変わりませんが、それらを何度でも実行できるので、処理全体としては30秒制限も関係無くなりますしね。今回の処理だと、以下のような手順を行います。

  1. schemaVersion == 1の条件で移行対象のエンティティを取得する。
  2. 念のため、それらのエンティティのバックアップを保存しておく。
  3. UserId(Twitterのユーザ)で集約し、Twitterのユーザに対応するscreenNameを取得して、そのユーザに該当するエンティティを以下のように更新する
    • 取得した screenNameを設定
    • スキーマバージョンを2に設定
  4. 更新したエンティティをデプロイ環境へ保存する。

それぞれの手順内の細かい処理は省いたスケルトン部分のコードは次のようになります。

setUpBeforeClass(); // ローカルのAppEngine環境を開始する
try {
  // デプロイ環境のMakeSyncCallServletに接続するためのアカウント情報を入力させる
  getAccountInfo();
  // 移行対象のエンティティを取得する
  final List<Tweet> tweets = getOldEntities(); 
  // ローカルのデータストアにバックアップを作成する
  backupToLocalDatastore(tweets);
  // データ移行済みのエンティティを作成する
  final List<Tweet> updated = createUpdatedEntity(tweets, 50);
  // デプロイ環境側のデータストアに、データ移行済みのエンティティを保存する
  MakeSyncCallServletDelegate.runInDelegateWithAuth(new Runnable() {

    @Override
    public void run() {
      executeBatch(updated);
    }
  }, email, password, SERVER, SERVLET);
} finally {
  tearDownAfterClass(); // ローカルのAppEngine環境を終了する
}

デプロイ環境との直結には、デプロイ側にMakeSyncCallServlet、ローカル側MakeSyncCallServletDelegateという、自作のリモート接続の仕組みを使っています。ソースコードの全体は次のリンクから見る事ができます。このソースだと、AppEngineの制限よりもTwitterの制限が回避できなくて困ったりするんで、そのあたり上限の件数を設定したりしていますけど、実際にアプリ内の用件でバッチ処理を行うときにはあまり関係ないですね。あと、処理後にデプロイ環境で使われているMemcacheを全てクリアしたりもしています。プロダクトコード内の処理が走らない限りMemcacheにキャッシュされたデータを使用する設計も多いだろうし、そういう事も想定されるならMemcacheのクリアも忘れずに実行しておかないといけません。

MakeSyncCall関連については以下の資料を参考にしてください。LT用であんまり詳しくはないですけれども、大枠は資料の通りです。実際にはこのサーブレットをweb.xml内でセキュリティをかけて配置しています。

ちなみに、この直結の仕組みを使うとクライアント側アプリケーションから直接Datastore.query()..とかDatastore.put()...といった操作をする、昔のクラサバのような組み方でアプリケーションを作れるって事ですね!

2009年11月2日月曜日

#friendfeed #twitter #rss クライアント: 空うさぎ

Growl風の通知機能と、通知のためのフィルタ機能が強力なクライアントソフトが公開されています。

FriendFeedへのPostは主にコメントのPostがメインで、TwitterへのPostは通常のPostとリプライがメインのようです。メインはフィルタを設定しての通知機能です。

FriendFeedクライアントの決定打が見つからなかったという事でも便利ですが、自分はRSSの機能も活用する事で以下のように使っています。

  • FriendFeedのクライアントとして。PostはコメントとLikeを使っている。
  • Twitterのクライアントとして。流量が多いので、キーワードに応じて通知の強さを分けるようにしている。
  • GMailのFeed機能を利用した、inboxへのメールの通知→AppsのGmailもFeedに対応しているので、GmailNotify系は引退させた
  • 会社で使っているRedmineのProjectタイムラインの通知
  • 個人的に使っているTracのProjectタイムラインの通知
  • 情報収集用のGoogleAlertの通知
  • 情報収集用のTwitter検索→TweetDeckは引退させた。

今後はASlimTimerがTimeTrackをSlimTimer以外にもPostできるようになって、空うさぎがそれの通知をできると理想的な世界がくるかも。ASlimTimerを使ったプロジェクト/社内での「すべてのアクティビティの可視化」と、空うさぎを使った「メンバ内でのアクティビティの状態の共有」が進むといいなぁ!

2009年10月17日土曜日

#appengine java night #2( #ajn2 )に参加した

ゲットしたノウハウ

  • JDOのEntityGroupで、親Entityに子Entityを保持するOwnedと、KeyのみでのEntityGroupの構築、でパフォーマンスに倍くらいの差がある。
    • これは気づいてなかった!
  • OpenCVの存在

@yuroyoroがスピーカーの勇姿を激写した

感想

参加人数が前回よりはるかに多くなりそうだった&経験者比率が低くなりそうだったので、前回ほどプレゼン中の質問とかが出ないかも?という心配をしていましたが、結構質問も出ていいカンジになってくれて良かったです。ぶいてくのたけざきさんがいい具合に突っ込みを入れてくださったのにも助けられました。また前回同様、ところどころひがさんが補足or質問してくれる形式は良いですね。また、今後発表者となる方は、資料は普段の1.5-2倍時間がかかると思ってくださいw 今回は1.5倍かかりますた。

shin1ogawaの発表資料

自分の内容は経験者向けのトーク、という事で未経験者や前回参加していない方には不親切だったと思います。そのあたり申し訳なかったです…といいつつ、今後スピーカーをやるとしても、そういった路線でやっていきますのでよろしく!
とはいえ私での不手際もやってしまいました。会場のタイムテーブルの流れにあわせて本編で話すべき自動テストのプレゼンとLTを入れ替えて話したんですが、LTの方は自動テストの話ありきで書いていたのです。それなのに逆にしてしまった。そんなワケでLTに関してはかなり「?」となった人も多かったと思います、すんませんすんません。LTの方は、「RemoteAPIの代わりとして使える仕組みを作ったよ!」て話でした。

感謝

めちゃくちゃカッコイイ会場を提供してくださった株式会社リクルート様、リクルートメディアラボの川崎様、大変ありがとうございました。また今回も開催に関するしきりをしてくださったスティルハウス佐藤さん楠元さん(10/19追記:漢字を間違っていたので修正しました、失礼しました)、貴重なお話をしてくださったひがさん、ありがとうございました!

宣伝

マッシュアップと言っても、以前のような「複数のサービスを組み合わせる」といった意味ではなく、最近は「いろんなPlatform、デバイスで動作させる」などなど広い意味で使われるそぅです。対象となっているAPI、Platformのどれかひとつでも使っていれば参加できるそーなので、皆さんも参加してみましょう!もちろん、Google App Engineも対象です。…というワケで、自分も登録しました。嫁ちゃんとペアで参加しようかなーと思います。

2009年10月13日火曜日

[宣伝] #appengine javaの入門者向けのセミナーを開催します

Google周りを触っている会社として売っていこう!という会社の取り組みで、無料セミナーをやる事になりました。

これはappengineの入門者向け(Javaエンジニアが対象)なので、appengine java nightに参加されているような猛者の方とかは来てもたぶん面白くないです。AppEngineの嬉しいトコロか、今までのJavaアプリとの違い・そこからくる注意点とか、んじゃどんなカンジに作ろう?とかの触りの部分、おおまかな概要を1時間程度でかるーく説明する予定です。
もしまだappengineをあまり触っていなくて、触る前に「appengineってどんなカンジなのか?」をザックリと知りたい、という方にはちょうど良い内容なんじゃないかと思いますんでぜひご参加ください。申し込み方法はatndのページで参加登録をして、そこにある登録フォームへのリンクからも登録情報を入力してください。ちょっと面倒な手順となってしまい申し訳ないですがよろしくお願いします。
今後の予定としては、3日間ほどかけて実際にひととおりの機能を実装(主にDatastore周りになるでしょうね)していくような有料セミナーも開催したいなーとか考えていたりします。

もうひとつ宣伝

また弊社ではAppEngine以外にはAppsもやっていて、それについて弊社代表の加藤が入門者向けにAppsの無料セミナーをする予定です。こちらは16日(金)に開催する予定で、Appsの導入を迷っておられる経営者の方が対象のようです。詳細は下記URLからご確認ください。