2009年11月24日火曜日

@bufferings さんによる #appengine #slim3 データストアドキュメントの日本語訳

bufferingsさんがやってくれました。このドキュメントはslim3についての説明というだけではなく、Google App Engineのデータストアの詳しい説明にもなっており、とてもわかりやすくなっています。オススメ。

2009年11月19日木曜日

#appengine hack-a-thon #1に参加した

もう二日も経ってしまったので、今更かよってカンジですけれども。

感想

六本木のアカデミーヒルズ40Fで開催されました。キレイ&眺めが良いフロアで、からあげ弁当・麻婆なす弁当がそれぞれ\600と\500、喫煙ルーム付き、というかなり良い環境でした。コミュニケーションは途中の昼食、終了後の懇親会がメインで、作業中はとにかくもくもくと作業するカンジ。
オンラインのコミュニケーションは、事前の予定通りTwitterで#ahack1 のハッシュタグを使おう!ってやってましたが、主に「富士山」「チョコレートの配給」「座った席の連絡(松尾さんを目印に)」「芋の配給」という話題ばかりが流れてましたw ついでに書いておくと、事前に用意したリポジトリは実に役に立ちませんでしたw でも、それなりの成果も上がっていましたし、色々と濃い話もできて楽しかったです。

私の成果

maven+eclipse(with Google Plugin for Eclipse)を使っている場合に、appengineのプロジェクトを簡単に作成するプラグインを作っていました。当日はslim3datastore+servletのシンプルなプラグインが完成し、slim3datastore+wicket用のプラグインの途中までで時間切れ。帰宅後にそれを完成させ、次の日にslim3datastore+cubby用のプラグインを完成させて、このエントリを書いています。

このプラグインを使うと以下の3手順ですぐに appengine のプロジェクトができあがり開発がすぐに開始できます。

  1. mvn archetype:generateして
  2. mvn eclipse:eclipse して
  3. eclipse に取り込む

この手順で作成したプロジェクトでできる事は以下の通り。

  • eclipseからGooglePluginを使って開発用のWebコンテナ上でアプリケーションを起動する(起動構成も生成されます)
  • eclipseからappengineの環境を利用する自動テストを実行できる
  • mavenからも自動テストを実行できる
  • makeSyncCallServletが組み込まれ、設定済みの状態になっている(デプロイ環境ではアプリケーション開発者権限を持つアカウントのみがアクセスできる設定になっています)

できない事は以下の通り。

  • JDOは一切組み込まれません。
  • mavenから開発用サーバを起動できない。mavenから開発サーバを起動するプラグインが1.2.6に対応しているかどうか未確認のため、組み込んでいません。

一番軽い構成になっているgaej-simple-pluginをが、wicketやcubby以外のフレームワーク用に拡張するのに便利だと思います。

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を使ったプロジェクト/社内での「すべてのアクティビティの可視化」と、空うさぎを使った「メンバ内でのアクティビティの状態の共有」が進むといいなぁ!