2010年2月15日月曜日

App Engine SDK 1.3.1 (overview)

2/10に1.3.1がリリースされていました。すっかり遅れた情報な上に内容もイマイチ把握できていませんが、一応書かなきゃなということで。
残念なことに調査に時間が取れずにいます。申し訳ないです。...とはいえ、これでも既に数時間は費やしていますが。
  • データストアのクエリのカーソル
     http://code.google.com/appengine/docs/python/datastore/queriesandindexes.html#Query_Cursors
     1.2.8から隠れていた機能がようやく正式なものになったようですね。

  • トランザクションタスクの作成
     なんでしょう。今までのタスクキューではトランザクションはサポートしてなかったのかな?

  • カスタム管理コンソールページのサポート
     なんでしょう。管理コンソールに独自のページでも作れるんでしょうか?

  • cron設定に新しい構文 "month"と"synchronize" の追加
     http://code.google.com/appengine/docs/java/config/cron.html
     monthは毎月を示しますが、以前からあったような?なんでしょう。
     synchronizeは毎日や毎時などの周期設定の場合に、開始時間の周期を一定させる動作になるようです。job開始時間からの周期になります。synchronizeの指定がない場合はjob終了時間からの周期になります。

  • SDKにアプリケーション統計ライブラリを追加
    http://code.google.com/appengine/docs/python/tools/appstats.html
     パフォーマンスを測定できるようになるようです。python版だけの機能です。

  • Bulk Loaderが全てのKindの一括ダウンロードをサポート
     これまでは1つずつ指定しないといけなかったようです。ただし開発サーバーではサポートしていないそうです。

  • appcfg.pyアップロードでHTTPS用のSSL証明書を検証
     証明書はAppEngineで持っているはずじゃ?ちょっと良くわかりません。URLフェッチで独自にkeystoreを渡さなきゃいけなくなったのかな?

  • HTTP HEADコマンドでEtagsと一致させるだけでなく、静的ファイルに304(Not Modified)のステータスコードを適用できるようになった
     http://code.google.com/p/googleappengine/issues/detail?id=575
     おお!ようやく!静的ファイルを扱うAppEngine内のサーブレットがちょっとヘボくて自前で304を返すフィルターなんぞを作ってましたが、これでもう大丈夫ですね。静的画像表示のもっさり感も大分軽減しそうです。
追々、メンバーが詳しく調べて投稿してくれるでしょう(ホントかな?)。いちおう期待。

2010年2月4日木曜日

1.3.1 SDKのPrereleaseが出ましたね。

とりあえず、ここをチェック。
prereleaseなので、まだ正式じゃないですが、開発者のみんな試してねということで、早めに出してるようです。
こちらでいち早く紹介されてます。情報速いなぁ。

2010年1月13日水曜日

Google App Engine実践リファレンス

技術評論社から、Google App Engine実践リファレンスが出版されました。弊社から出している、Google App Engine for Java [実践]クラウドシステム構築とターゲットにしている環境は一緒ですが、スタンスが違う書き方がされていて、面白いと思います。社内でも読んでみましたが、以下のようなことがいえると思います。
まず、この本をお薦めしたのは、以下のような人です。
  • これからApp Engineを使って「プログラミング」をしてみたいと思う人
  • 簡単なウェブアプリケーションJavaで書いたことがある人
  • 英語や日本語の文章を読むのが得意でなく、プログラムを写経して動作を確認しながら理解を深めたい人
対して、以下のような人はちょっと物足りないと感じるかも知れません。
  • App Engineを使ったシステムの「設計」や「評価」を行う人
  • データストアの挙動を根本から理解したい人
  • 弊社の本を読んで理解できた人
「リファレンス」というタイトル通り、この本では基本的な事を網羅的に記述しています。その反面、リファレンスの範疇から外れるプログラミングを行うのはちょっと難しいかもしれません。また、実際に動くコードがそれぞれ示されていて、すぐに動きを試すことができますが、初学者は少し異なることをして問題に直面した場合に、この本だけでは対応やその問題の本質的な理解は難しいかなと思います。ただそのような場合に、「困る」ことでいろいろな情報の調べ方やトラブルの切り分け方を学べるので、それもアリかなと思います。App Engineの世界は取っつきにくく、なんとなくよくわからないという印象を持つ人も多いと思いますが、このリファレンスを入り口にして、実際に小さなプログラムを動かすことで、App Engineの世界に入る敷居が下がるといいなと思います。

2010年1月6日水曜日

グローバルトランザクション処理のパターン

送金のトランザクション処理パターンでは、Google App Engine (GAE)のEntity Groupにまたがるトランザクション処理を行う方法について紹介しました。また、それに少しだけ最適化を施した結果、下図のような処理になりました。

しかし、このトランザクション処理はいくつかの制約があります。

  • (a) 送金中に合計金額がずれる
  • (b) 送金先の口座に制約をかけられない

このトランザクションはEventual Consistency (結果整合性)というレベルの整合性保証しかしないため、2つのEntity Groupの値にずれが発生する場合があります(a)。たとえば、口座(A)から口座(B)に1000円だけ送金する場合、(1)と(2)の間は「口座(A)から出金したが、口座(B)に入金されていない」という状態になります。

また、送金元の口座に制約はかけられますが、送金先の口座に制約をかけられません(b)。たとえば「残高不足で送金できない」といった処理は可能ですが、「残高が限界を超えたために入金できない」といったケースに対応できません。もしそこに制約をかけてしまうと、出金したけど入金できないという状況になってしまいます。

今回は、上記のような問題を解決したApp Engine上のトランザクション処理のパターンについて考えてみたいと思います。例によってドラフトなので、ご指摘等いただけると助かります。

なお、「グローバルトランザクション」という名前は、GAEの「ローカルトランザクション」に対して付けた語で、このエントリでは「Entity Groupにまたがるトランザクション」の意味で使います。GAEのEntity Groupのように、局所的にトランザクション処理を実行することをローカルトランザクションと呼ぶようです。

エンティティの書き込み

前回同様に、口座Aから口座Bに5000円送金する例について紹介します。スタートは下図のような感じです。

それぞれの口座を別のEntity Group上に配置し、さらに中央にもう一つ空のEntity Groupを用意しました。これについては次で。

トランザクションの開始 (begin)

まず、グローバルトランザクションを開始します。 このとき、グローバルトランザクションを管理するためのエンティティを一つ作ります。

これには、トランザクションID、開始時刻、状態、などのプロパティを持たせます。トランザクションIDはユニークな値なら何でも構いません。

ジャーナルの作成 (prepare)

次に、変更対象のエンティティを取得し、変更後にジャーナルとして書き込みます。エンティティに直接上書きしないのは、どこかで失敗した際に正しく復元するためです。 ジャーナルには、どのトランザクションで作成されたものかがわかるように、先ほど作成したトランザクションエンティティのIDを付与しておきます。

同様に、残りのエンティティに対しても変更してジャーナルを作成します。これらのエンティティに対する変更は同時に行ってもかまいません、 このため、前回の「(b) 送金先の口座に制約をかけられない」という制約を回避することができます。

なお、上記の2つのエンティティの取得にはtransactional readを使っていません。 もし、同時刻のスナップショットがほしければ口座エンティティにバージョン情報を振っておき、「口座(A) -> 口座(B) -> 口座(A)」の順に読みだして、口座(A)のバージョン整合性を確認すればよさそうです。バージョンが一致した場合、口座(B)を取得した時刻の口座(A)と、読みだした口座(A)の内容が完全に一致します。

トランザクションの完了/中止 (commit/abort)

全てのジャーナルの書き込みが成功したら、トランザクションエンティティの状態を「開始」から「完了」に変更します (「開始」以外を「完了」に変更すると不整合が起こります)。

ここでのポイントは、この状態が「完了」になると全てのジャーナルの情報が有効になる、という点です。 逆に、それまではジャーナルの情報は有効になりませんので、トランザクション全体の状態をここで管理することで、全体の変更をトランザクショナルに行っているように見せかけています。

ジャーナルの書き出しが一つでも失敗したら、トランザクションエンティティの状態を「開始」から「失敗」に変更します。これは「完了」と逆の処理で、全てのジャーナルを無効化します。 この「失敗」の情報を書き出せなかった場合の補償については、後の「トランザクションのタイムアウト」で紹介しています。

ジャーナルの適用 (roll forward)

トランザクションエンティティの状態が「完了」になったら、先ほど書きこんだジャーナルの情報を有効にできます。ジャーナルの情報をエンティティに上書きして、残高を10000円から5000円に変更(5000円出金)します。 ジャーナルの反映が終わったら、同一のローカルトランザクション内でジャーナルの情報を削除します。このため、ジャーナルの情報がエンティティに反映されるのはこの1回だけです。

同様に、残りのエンティティについてもジャーナルの反映を行います。こちらでは残高を10000円から15000円に変更(5000円入金)します

なお、上記の処理も「べき等 (idempotent)」に行うことができます。つまり、何回実行してもジャーナルが反映されるのはちょうど一度だけです。 このため、トランザクションエンティティの状態が「完了」になれば、あとは成功するまでジャーナルの反映を繰り返せば、グローバルトランザクションはいつか成功します(するはずです)。

このべき等性を利用して、App Engineではジャーナルの適用にTask Queueを使うこともできます。 トランザクションエンティティを「完了」にする際に、ジャーナルの適用を行うためのタスクを登録すれば、これらのジャーナルはいつか必ず適用されます。 ローカルトランザクションの中でタスクの登録を行う方法は、「DBオペレーションに応じてメール送信したい - Kays daddy」などでも紹介されています。GAE for Javaの場合は、トランザクションオブジェクトを引数にとるQueue.add()メソッドを利用します。

このときに注意すべき点として、適用するジャーナルは現在のトランザクションに関するものでなくてはなりません。何らかの理由でジャーナルの適用が2回行われ、2回目には別のトランザクションのジャーナルが作成されていた場合、勝手に適用すると大変なことになります。

トランザクションのタイムアウト (timeout)

今回は、トランザクションのタイムアウトについても考えておく必要があります。トランザクションエンティティの状態が「完了」になった以降はタイムアウトしなくてもよいのですが、「ジャーナルを作成してから、トランザクションエンティティの状態が「完了」になるまで」の間にエラーが起こった場合、ジャーナルがゴミとして残ってしまいます。

以降で詳しく説明しますが、ジャーナルが作成されて反映されるまでの間、それぞれのエンティティへの書き込みはできません。書きこんでしまった場合、そのあとに行われるジャーナルの反映で、その書き込みは上書きされてしまいます。 つまり、ゴミとして残ったジャーナルをそのままにしておくと、エンティティを利用できなくなる可能性があります。

そこで、状態が「開始」のまま一定以上時間がたったトランザクションについては失敗したものとみなすようにします。

なお、GAEでは1回のリクエストに30秒以上掛かるものは強制的に終了させられるため、「開始から30秒たっても完了しないトランザクション」を対象にするのがよさそうです。

確実にタイムアウトの処理を行いたい場合には、トランザクションの開始時に「30秒後にトランザクションをタイムアウトにするタスク」を(トランザクショナルに)登録しておけばいつか必ず実現できます。

ジャーナルの破棄 (rollback)

トランザクションエンティティの状態が「失敗」になったら、それぞれのジャーナルの情報は破棄します。

つまり、トランザクションが「完了」になったら全てのジャーナルを反映、トランザクションが「失敗」になったら全てのジャーナルを破棄、といった形になります。 このような形で、トランザクション処理の特性である「アトミック性」について実現できました。

なお、このロールバックの処理についてもジャーナルの適用と同様にTask Queueを利用して実現することもできます。 トランザクションの状態を「失敗」にすると同時にタスクを登録しておけば、いつか必ずロールバックが成功します。 ロールフォワード時の注意点と同様に、、破棄するジャーナルは現在のトランザクションに関するものでなくてはなりません。

エンティティの読み出し

今回はジャーナルという仕組みを使ったため、読み出しについてもいくらか複雑になっています。 トランザクションの状態を4種類に場合分けして、それぞれについて読み出し方を考えてみます。

ここで、適切な分離を選択すれば、前回の「(a) 送金中に合計金額がずれる」という問題を解決できます、が、それなりに大変そうです。

説明が複雑になるのを避けるため、以下すべてローカルトランザクションで、口座エンティティとジャーナル情報を読みに行ったものとしてください。

ジャーナルが存在し、トランザクションが「開始」

トランザクションの状態が「開始」である場合、そのトランザクションはその後に「完了」か「失敗」かのいずれかになります。 「完了」と「失敗」のどちらになるかによってジャーナルの意味が変わってしまうため、現時点でそのエンティティを使うのは危険です。

分離レベルによってはこのエンティティをリードオンリーで利用してもいいかもしれません(トランザクションエンティティをtransactional readしていないので少し怪しいです)。 いずれにせよここでエンティティを変更した場合、その後にジャーナルで上書きされてしまう恐れがあります。

ジャーナルが存在し、トランザクションが「完了」

先ほどの「開始」状態と異なり、「完了」の状態ではジャーナルの情報が有効になっています。 次に必要な操作はジャーナルの適用となるため、読み出し時にはエンティティの情報を無視して、ジャーナルの情報を利用します。

読みだしたエンティティを書き戻す場合は、同時にジャーナルを削除しておきます。これを忘れると、次に読み出す際にもジャーナルから情報を復元してしまい、いつまでたっても書き込みが反映されません。

ジャーナルが存在し、トランザクションが「失敗」

トランザクションの状態が「失敗」の場合は、ジャーナルの情報が無効になっています。 次に必要な操作はジャーナルの破棄となるため、読み出し時にはジャーナルの情報を無視してエンティティの情報を利用します。

このエンティティを書き戻す場合は、先ほどと同様に同時にジャーナルも削除します。

ジャーナルが存在しない場合

ジャーナルが存在しない場合、そのエンティティはグローバルトランザクションに参加していないか、グローバルトランザクションに参加しているもののジャーナルの作成が行われていない状態です。

このケースでは、普通にエンティティを利用できます。

ジャーナルが存在しないエンティティを読み出した後に、そのローカルトランザクション内で変更して書き戻すこともできます。 そこでの整合性については、ローカルトランザクションの機能(楽観的並行性制御)を利用して保障します。 たとえば、別のグローバルトランザクションにおけるジャーナルの作成とこの書き戻しが競合し、先にこの書き戻し処理が成功した場合には、同時に進んでいたグローバルトランザクション全体が失敗します。

なお、ジャーナルが存在する場合には常にトランザクションが「開始」の状態であるとみなす、という別の戦略をとることもできます。 ただし、その方法ではジャーナルによるブロッキングの時間が長くなってしまいますが、トランザクションエンティティの状態を確認しに行かなくてもよくなるために、読み出し処理のパフォーマンスを向上させることができます。この辺りはトレードオフになると思います。

その他

最適化について

今回のエントリで紹介したやり方は、理解しやすさを優先したためかなり無駄が多いです。 トランザクションエンティティが個別のEntity Group上に作成されていますが、これを変更対象のエンティティがいるEntity Group上に作成してやると、ローカルトランザクションの回数を数回削減できます。 また、トランザクションエンティティの「開始」や「失敗」は、ジャーナルエンティティにタイムスタンプを埋めておくことで代用できそうです。

上記のやり方では、Entity Groupがn個のグローバルトランザクションを実現するために、2n - 1回のローカルトランザクションが最低でも必要になりそうです。 送金のトランザクション処理パターンおよびその最適化によって実現したトランザクションは、ちょうどn回のローカルトランザクションで済みますので、状況によって使い分けるのがよさそうです。

まとめ

今回は「送金のトランザクション処理パターン」で紹介した方法よりも、より細かい制御が可能なグローバルトランザクションの手法について考察してみました。 これをアプリケーション側で書くとなると非常に大変そうなので、公式のSDKやフレームワークなどで同じようなことができるようになれば、Google App Engineでもう少し無茶ができそうです。

いずれの場合にせよ、整合性を保障する方策や、許される整合性のレベルなどは、常にACIDトランザクションを利用していた設計よりも多くのことを考えなければなりません。 このエントリで紹介した方法は、パフォーマンスに悪影響を及ぼす可能性が考えられますので、必要な整合性や設計および実装の手間を考慮しながら、注意深く選択することが必要になります。

2009年12月28日月曜日

App Engine JDO Tips

Google App Engineのデータストアを利用する場合、公式にはJDOおよびJPAが提供されています。 過去にJDOからSlim3 Datastoreに乗り換えるようなことも書いていますが、最近のJDOについて少し書いてみたいと思います。

今回は、Google App Engine Blog: JPA/JDO Java Persistence Tips - The Year In Reviewで紹介されている内容をもとに話を進めます。JDOの使い方自体に詳しくなくてもある程度読めるように書いたつもりですが、何かあればコメントください。

相互参照する one-to-many の関係 (episode 1, 3)

JDOを利用すると、相互参照を行うone-to-manyの関係を簡単に記述できます。

下記はParentクラスのオブジェクトが、複数のChildクラスのオブジェクトを保有する例です。

package com.example;

import java.util.*;
import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Parent {

    @PrimaryKey
    private String keyName;

    // Childクラスのparentプロパティが自身への参照を持つ
    @Persistent(mappedBy = "parent")
    @Element(dependent = "true")
    private List<Child> children = new ArrayList<Child>();

    // ...
}
package com.example;

import javax.jdo.annotations.*;
import com.google.appengine.api.datastore.Key;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Child {

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    private String value;

    // Parentへの参照を保持して相互参照させる
    private Parent parent;

    // ...
}

上記のように書いた場合、ParentクラスのchildrenにChildクラスのオブジェクトを設定するだけで、自動的にそれぞれのChildクラスのparentフィールドにはParentクラスのオブジェクトが設定されます。

また、上記のような owned-relationship と呼ばれる関係は、ParentをルートとしたEntity Groupを自動的に構成します。このため、Childクラスのオブジェクトを持つParentはトランザクション処理による変更が可能です。

このEntity GroupはKeyのパスで表現されます。Childクラスのエンティティは親を持つことになるので、@PrimaryKeyの指定を、(String, Longなどではなく)Key型のフィールドに対してする必要があります。

なお現在の実装では、上記のようなParentを保存する際にchildrenというプロパティはデータストア上に作成されません。そのかわり、Childを保存する際にchildren_INTEGER_IDXというプロパティがデータストア上に作成され、Parentから見たChildの位置(children内のインデックス)が保持されます。

このため、過去のParentと同じキーを持つParentオブジェクトを生成し、childrenを操作してしまうと過去のchildrenの内容がマージされるという残念な状況になります。つまり、2つのChildオブジェクトを持つParentを2回作成すると、あとでParentを取得した際に4つのChildオブジェクトを持っていたりします。

そのため、上書きする場合でも一度取得したオブジェクトを利用します。

PersistentManager pm = ...;

Parent parent;
try {
    // すでに存在したら再利用
    parent = pm.getObjectById(Parent.class, "parent");
}
catch (JDOObjectNotFoundException e) {
    // 存在しなければ新しく作る
    parent = new Parent();
    parent.setKeyName("parent");
    pm.makePersistent(parent);
}

Child child1 = new Child();
Child child2 = new Child();

child1.setValue("child-1");
child2.setValue("child-2");

// ChildオブジェクトをParent.childrenに追加
List<Child> children = parent.getChildren();
children.add(child1);
children.add(child2);

JDO(とJPA)はtransparent persistence (透過的な永続化)を提供するためのライブラリです。上記のように新しく作成したオブジェクトはすぐにPersistentManager.makePersistent()によって永続化対象に指定し、そのオブジェクトを使い続けるスタイルのほうがやりやすいかもしれません。

キーのみを取得するクエリ (episode 4)

キーのみを取得するクエリが利用可能になっています。 やりかたは、JDOQLを書く際に"SELECT <キーを保持するフィールド名> FROM ..."のように、@PrimaryKeyの指定をしたフィールドを指定するだけです。

PersistentManager pm = ...;

// SELECT <キーを保持するフィールド名> FROM ... でキーのみを取得
Query query = pm.newQuery("SELECT key FROM " + Hoge.class.getName());
List<Key> keys = (List<Key>) query.execute();
...

App Engineのクエリは、(1)キーの一覧を取得、(2)キーからエンティティを取得、の順で結果を取得しているようなので、キーのみを取り出す場合には上記のように書くことで、(2)の処理をスキップすることができます。

エンティティの取得とモデルへのマッピングはそれなりにコストが高い操作ですので、キーだけが必要である場合には便利です。特に検索のみに利用するエンティティなどは、キーに検索結果としての有効な情報を凝縮することで、不要な情報をロードしなくて済みます。

シリアライズしたオブジェクトの格納 (episode 5)

App Engineのデータストアに利用できるデータの種類は限られていますが、JDOを利用すると自動的にオブジェクトのシリアライズを行ってBlobとして永続化できます (Blob StoreのBlobとは別物です)。

package com.example;

import java.util.Map;

import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class ObjectHolder {

    @PrimaryKey
    private String keyName;

    // MapはJDOで扱えないので、serializedを指定する
    @Persistent(serialized = "true")
    private Map<String, String> map;

    // ...
}

@Versionを利用したLong Running Transaction (episode 6)

App Engineのデータストアにはトランザクション処理を行うための仕組みがあらかじめ組み込まれていますが、リクエストごとにコミットまたはロールバックしなければならないため、画面をまたぐような長期間の並行性制御(Long Running Transaction)はソフトウェアで指示する必要があります。 JDOにはこれを自動的に行う仕組みが備わっています。

まず、JDO向けのモデルクラスに@Versionというアノテーションを付与します。

package com.example;

import java.io.Serializable;

import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
@Version
public class Versioned implements Serializable {

    private static final long serialVersionUID = 1L;

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    private String value;

    // ...
}

このとき、detachable = "true"という指定と、implements Serializableを併せて指定しておきました。前者はオブジェクトをJDOの管轄から外してスナップショットを作成する(デタッチ)のための指定で、後者はセッションに同オブジェクトを保持しておくための指定です。

Long Running Transactionを開始する場合には、下記のように@Versionを付与したモデルにPersistenceManager.detachCopy()を実行して記憶しておきます。

PersistenceManager pm = ...;
Versioned data = ...;
data.setValue("Hello!");
pm.makePersistent(data);
// 現在の状態のスナップショットを作成
Versioned snapshot = pm.detachCopy(data);
// snapshotをセッションなどに保存

それぞれの画面で行われた変更は、全て上記のデタッチしたsnapshotに対して行います。 全ての操作が終わったら、最後にデータストアに書き戻します。

PersistenceManager pm = ...;
Versioned snapshot = ...;
pm.makePersistent(snapshot);

上記のように、JDOの@Versionを利用したLong Running Transactionでは、明示的にJDOのトランザクションを開始する必要はありません。というより、明示的にJDOのトランザクションを利用すると、@Versionによる並行性制御が無視されます

これはそれなりに有名なことらしいのですが、知らないと非常に不可解な動きをします。このエントリを書いてる最中に同じようにはまって、様々な方に助けていただきました(いつもありがとうございます)。

このLong Running Transactionは、PersistenceManager.close()を実行した際に実際の反映が行われます。もし、スナップショットを書き戻すまでに外部から(本体の)モデルが変更されている場合、close()が実行されるタイミングでJDOExceptionがスローされていました。

このPersistenceManager.close()の中では、おそらく下記のような操作が実行されています。

PersistenceManager pm = ...;
SomeModel newData = ...;
...
Transaction tx = pm.currentTransaction();
try {
  // 現在のトランザクションを利用して、同じIDのオブジェクトを取り出す
  SomeModel oldData = pm.getObjectById(SomeModel.class, newData.getId());
  // サーバから取り出したバージョンと、手元のバージョンを比較
  // 他の人が変更していたらサーバから取り出したバージョンがずれているはず
  if (oldData.getVersion() != newData.getVersion()) {
    // 実際にはJDOExceptionがスローされる
    throw new ConcurrentModificationException();
  }
  // バージョンチェックと書き込みは同一トランザクションで
  // 処理するのでアトミックに行われる
  pm.makePersistent(newData);
  tx.commit();
}
catch () {
  if (tx.isActive()) {
    tx.rollback();
  }
}

より詳しい考察は、App Engineでバージョンによる楽観的排他制御 - ひがやすを blogなどでも行われています。

はまった点

@Versionの調査をしていてひどくはまっていた点を紹介します。

PersistenceManager pm = ...;
Versioned data = ...;
// (a) 先にmakePersistent()で永続化の対象に指定
pm.makePersistent(data);
// (b) そのあとにモデルを変更
data.setValue("Hello!");

// (c) 現在の状態のスナップショットを作成
Versioned snapshot = pm.detachCopy(data);
...

// (d) 最後にclose()
pm.close()

上記のように、最初にPersistenceManager.makePersistent()を実行(a)し、それ以降にモデルオブジェクトを変更した(b)場合、最後のPersistenceManager.close()の瞬間(d)にモデルオブジェクトのバージョンが変更されます。このため、(b)と(d)の間で行われたスナップショットの作成(c)は、(d)で変更されるバージョンの情報を反映していません(aの時点のバージョンを利用します)。

このようなスナップショットの書き戻しは常に失敗します。これは(d)の時点で変更されたバージョンが「Long Runnning Transactionの外部で変更が行われた」というように見なされてしまうためです。

makePersistent()detachCopy()を同一リクエストで行う場合、最初に紹介したように「すべて変更してからmakePersistent()を実行する」というようにした方がよさそうです。

PersistenceManager pm = ...;
Versioned data = ...;
// 先に変更してから
data.setValue("Hello!");
// makePersistent()で永続化の対象に指定
pm.makePersistent(data);
// すると、detachCopy()はmakePersistent()時点のバージョンと一致
Versioned snapshot = pm.detachCopy(data);

transparent persistence (透過的な永続化)とさっき書いたばかりなのですが、App Engineではどうもそう簡単にいかないようです。

インデックスを作成しないプロパティ (episode 7)

データストアにエンティティを格納した場合、基本的にはそれぞれのプロパティに対するインデックスが作成されます(TextやBlobなどは対象外)。しかしこのインデックス作成はCPUやデータストア領域などのリソースを消費するため、不要の場合には抑制させることもできます。

具体的には、インデックスの作成を抑制したいプロパティに対して、@Extension(vendorName="datanucleus", key="gae.unindexed", value="true")という長い記述の注釈を付与するだけです。

package com.example;

import javax.jdo.annotations.*;

import com.google.appengine.api.datastore.Link;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class BlogEntry {

    @PrimaryKey
    private String keyName;

    // title はインデックスを作る
    private String title;

    // permalink はインデックスを作らない
    @Persistent
    @Extension(vendorName = "datanucleus", key = "gae.unindexed", value="true")
    private Link permalink;

    // ...
}

上記のモデルに対してpermalinkをフィルタに指定するクエリを実行した場合、クエリは失敗するのではなく単に結果を何も返しません

なお、@Extensionにはのほかにorg.datanucleus.jpa.annotations.Extensionという注釈型も存在します。後者はJPA用なので注意が必要です (Enhanceの際にエラーが表示されます)。

インデックスの作成については、How Entities and Indexes are Stored - Google App Engine - Google Code (英語)が参考になると思います。

はまった点

今回のはまりは下記のような感じです。

package com.example;

import javax.jdo.annotations.*;

import com.google.appengine.api.datastore.Link;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class BlogEntry {

    @PrimaryKey
    private String keyName;

    private String title;

    // @Persistent を省略
    @Extension(vendorName = "datanucleus", key = "gae.unindexed", value="true")
    private Link permalink;

    // ...
}

プロパティに@Persistentを省略した場合、永続化対象にはなるけど"unindexed"の対象にならないという残念な挙動をします。このエントリでは基本的に@Persistentを省略していますが、可能な限り明記したほうがいいのかもしれません。

IN, != 演算子の追加 (episode 9)

SDK 1.2.8 Release Notesで語られなかったことでも書きましたが、SDK 1.2.8からはIN, !=という演算子が利用できるようになりました。下記のようにJDOQLの中で指定します。

Query query = pm.newQuery(Employee.class);
// 年齢が20歳でない
query.setFilter("age != 20");
List<Employee> results = (List<Employee>) query.execute();

...
// 特別な年齢の一覧
List<Integer> anniversary = Arrays.asList(20, 40, 60);
Query query = pm.newQuery(Employee.class);
// 年齢が「特別な年齢の一覧」に含まれる
query.setFilter(":anniversary.contains(age)");
List<Employee> results = (List<Employee>) query.execute(anniversary);

...

その他、気付いたこと

Google App Engine Blog: JPA/JDO Java Persistence Tips - The Year In Reviewで紹介されなかったものの、App Engine for Javaのリリース当初から変わっていたことについて、いくつか紹介します。

PersistenceManagerFactoryの初期化が高速化

Slim3 Datastoreに乗り換えるという記事では、JDOをやめた理由の一つに「初期化が遅い」ということを挙げました。 しかし、SDK 1.2.8より利用可能になった<precompilation-enabled>true</precompilation-enabled>という設定により、この初期化のコストをかなり削減できるようです。

このプリコンパイル設定をonにしたりoffにしたりして、それぞれの初回アクセス時の負荷を測定してみました。いずれもPersistenceManagerFactoryを初期化し、pmf.getPersistenceManager()を起動するだけの簡単なプログラムです。

プリコンパイル設定なし プリコンパイル設定あり 削減された時間
1回目 4433cpu_msec 2450cpu_msec 1983cpu_msec
2回目 4394cpu_msec 2741cpu_msec 1653cpu_msec
3回目 4588cpu_msec 2585cpu_msec 2003cpu_msec
4回目 4725cpu_msec 2741cpu_msec 1984cpu_msec
5回目 4510cpu_msec 2605cpu_msec 1905cpu_msec
平均 4530cpu_msec 2624cpu_msec 1906cpu_msec

上記のように測定回数は5回と多少心もとないですが、いずれも2000cpu_msec近い時間がプリコンパイル設定を有効にすることで削減されています。 体感的にも応答に5秒かかっていたものが3秒くらいになっている感じがしますので、上記のプリコンパイル設定は有効にする価値がありそうです。

コアデータタイプがデフォルトフェッチグループに追加

過去のJDOでは、次のようなモデルクラスを作成した際に、linkフィールドやmailsフィールドにはデフォルトフェッチグループの指定が必要でした。 デフォルトフェッチグループとは、特別な指定をせずにエンティティをデータストアから読み出した際に、自動的にフェッチされるプロパティ群のことです。 プリミティブ型、String型、Date型などはJDOの定義でデフォルトフェッチグループに含まれていましたが、App Engine特有のクラス(Key, Link, Email)などはそれらに含まれていなかったため、わざわざフェッチグループの指定をしないとデータストアからロードした際にnullのまま放置されていました。

package com.example;

import java.util.List;
import javax.jdo.annotations.*;
import com.google.appengine.api.datastore.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class CoreDataType {

    @PrimaryKey
    private String keyName;

    private Link link;

    private List<Email> mails;

    // ...
}

SDK 1.2.8 (datanucleus-appengine 1.0.4)からは com.google.appengine.api.datastore.* にある組み込みデータ型や、それらのコレクションについてもについてもデフォルトフェッチグループに追加されるようになったので、上記のモデルクラスはフェッチグループの指定なしに利用できます。

2009年12月18日金曜日

Google AppsアカウントでGAEが利用可能に。

気づくと、GAEのTOPページにこのような記述が増えていました。
Google Apps アカウントで Google App Engine にログインする場合は、次の URL を使用してください。 https://appengine.google.com/a/<YOURDOMAIN.COM>/
Google Appsアカウントで試してみました。 開発者として招待してみると、Googleアカウントで利用する際と違い、招待メールの内容にGoogleアカウントの承認リンクとは別にGoogle Appsアカウント用の承認リンクが追加されています。
To sign in with your Google Account and accept or decline this invitation, simply follow this link:
<Googleアカウント用承認リンク>
OR, if <メールアドレス> is a Google Apps Account, you can sign in and accept or decline this invitation here:
<Google Appsアカウント用承認リンク>
Google Appsを導入していて、これからGAEでの開発やデプロイして利用等には、アカウントの統一という面で利用する価値はありますね。

2009年12月17日木曜日

Google App Engine Downtime Notify でGAEのダウンタイムを知る。

 GAEを利用したアプリケーション開発・保守に関わっていらっしゃる方であれば、GAEへのデプロイ、テスト、運用時の障害というのは気にかかるところです。
 Google App Engine Downtime Notify ではメンテナンスによる事前のスケジュールも含めて、Googleからのダウンタイムの通知が行われており、このグループのメーリングリストに参加すると、Googleからの通知専用のメールが配信されます。
 配信時刻とダウンタイムの時間が太平洋標準時のため、日本時間の16時間前となることと、メンテナンスの予定の通知以外は、ダウン後のダウンタイムの通知になってしまうことについては注意が必要です。
※過去も投稿の時刻を読む限り、サマータイム時についても、そのままの時間で配信されているようです。
 最近では、12月3日の9:33(太平洋標準の12月2日16:33)に「Index definition update issue today」という投稿が配信されています。
「太平洋標準時でおよそ12:55からPM3:55の間に、App EngineのデータストアのIndex definitionsを変更しました。それによるアプリケーションやユーザーへの影響はありませんでしたが、開発者がアプリケーションのデプロイや、Index definitionsを更新しようとするとすると"500 エラー"が発生していたでしょう。この問題は現在、解決され、開発者はアプリケーションのデプロイやIndexの更新が出来る筈です。」
 と、原因や現在の状況をお知らせしてくれますので、何か(無いに越したことはありませんが)と助かるかと思います。GAEのメール受信機能を利用して障害報告のお知らせ画面を作成されている方も居るんじゃないでしょうか。
 また、今まさに障害かな?と感じた場合はGAE/Jの日本海外のフォーラム、またはGAEのフォーラムを覗いてみると、他の開発者が書き込んでいるかもしれません。