2010年6月25日金曜日

Google Appsがマルチドメインに対応

ずっと前から出来るようになると言われてて、漸く対応したようです。
早速、設定してみましたが、まだドメインのverify中。手順は、ここに書いてある通り。
ただ、ちょっと制限があって、
  1. postiniはシングルドメインでしか使えないため、ドメインエイリアスとして設定しないで、別ドメインを独自で使おうという場合には、postiniを外さないとダメ。→今回はpostini外すのが面倒だったので、エイリアスとして設定。
  2. 既存ドメインのアカウントは、新ドメインとマージされない。→マルチドメイン設定後に新しいアカウントを作る時には、どのドメインか選択可能。
postiniはちょっと残念。そのうち制限外されることを望みます。
対象のEditionは、PremierとEducation。学校に説明に行く時に、説明しやすくなったかも。

2010年6月3日木曜日

データストアが不安定な状態

ここ数週間、GAEのデータストアが不安定な状態が続いています。

管理コンソールが表示されなくなるのも、この問題が原因のようです。

上記の記事では、この不安定な状態の原因を「App Engineの急激な普及」と分析しています。具体的には、ここ半年で2カ月ごとに25%ずつサービスが増加するという状況らしく、それに伴うインフラの整備が追い付いていないようです (さらに、もう2週間くらいは少なくとも不安定な状態になるだろう、と付記しています)。

この状況を受けて、2010/5/31からデータストアのパフォーマンスが安定するまでの間、データストアのCPUに関する課金は行わないという発表がありました (保存しているデータ量に関しては触れられていないので、多分課金されます)。課金が再度行われるようになる際には公式のブログで公開するそうなので、要チェックです。

先ほどキャプチャしたデータストアクエリの遅延に関するグラフ(クエリのみ)です。真っ赤になってます。

2010年5月28日金曜日

Google App Engine for Java[実践]クラウドシステム構築」正誤表 第3章

p.86 リスト3.4.1

package mysite.server;

import java.io.IOException;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class LogFilterImpl implements Filter {

    private FilterConfig filterConfig;

    private static final Logger LOG =
            Logger.getLogger(LogFilterImpl.class.getName());

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain filterChain)
            throws IOException, ServletException {
        LOG.info("Log filter processed a " +
                 filterConfig.getInitParameter("logType") +
                 " request");
        filterChain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    @Override
    public void destroy() {
        // no-op
    }
}

2010年5月12日水曜日

NSArrayとNSDictionaryのオーナーシップ

Objective-C 2.0ではガベージコレクションが導入されて、メモリ管理がだいぶシンプルになりましたが、iPhone開発ではその恩恵に預かれませんので、色々と気を使います。

NSArrayに要素を追加した時、NSArrayはその要素オブジェクトにretainメッセージを送ってオーナーシップを獲得します。NSArrayが解放された場合、格納されているオブジェクトすべてにreleaseメッセージが送られます。また、格納した要素をNSArrayから削除した場合、その要素にreleaseメッセージが送られます。

なので、こういう書き方をするのはちょいとよろしくありません。

NSArray *array = [[NSArray alloc] initWithObjects:
                     [[Foo alloc] init], [[Bar alloc] init],nil];
// なんらかの処理
[array release];

FooとBarをallocしてinitした時点で、このコードが書かれているオブジェクトがFooとBarそれぞれのオーナーになります。その後、array に追加しているので、array もこの2つの要素のオーナーシップを持ちます。

array を release する事で、Foo と Bar にもreleaseメッセージが送信され、retainCount が減るのですが、コードが書かれてるオブジェクトがまだオーナーシップを握っているため、破棄されません。このコードだと破棄しようもない。困った。

なので、律儀にこう書くのが吉。

Foo *foo = [[Foo alloc] init];
Bar *bar = [[Bar alloc] init];
NSArray *array = [[NSArray alloc] initWithObjects: foo, bar, nil];
[foo release];
[bar release];
// なんらかの処理
[array release];

これじゃどんくさい、という人はautoreleaseにまかせてしまうとか。

NSArray *array = [[NSArray alloc] initWithObjects:
                     [[[Foo alloc] init]autorelease],  [[[Bar alloc] init]autorelease], nil];
// なんらかの処理
[array release];

iPhone環境はautoreleaseは結構まめにやってくれる事がこないだ判ったので、これで安心でしょう。

NSDictionaryの場合、こちらもNSArrayとほぼ同様なのですが、値はそのまま参照してretainメッセージを送りますが、キーはオブジェクトをコピーして保持するそうです。

私はどんくさい方が好みです。実生活ではぐうたらであまり掃除は得意でないんですが。

2010年5月7日金曜日

Java入門サンプル(1)

  • 注意
    • このエントリは実験中のJava入門教材のサンプルです
    • GAE の調子が悪かったりQuotaを超えてたりするとデモがうまく動かないかもしれません
    • 技術背景等はGooseあたりを参照してください

Hello, world!

まずは簡単なプログラムを作成してみましょう。Hello.javaという名前のファイルを作成して次の内容を書き込んでください。

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

このプログラムを実行するには、コマンドラインで次のように指定します。

cd (ファイルを作成したフォルダ)
javac Hello.java
java -cp . Hello

すると、「System.out.println("Hello, world!");」のところに書いた「Hello, world!」という文字列が画面に表示されたと思います。

Hello, world!

Javaのプログラムは次のような構造で書きます。

public class (ファイルの名前) {

    public static void main(String[] args) {
        (命令)
    }
}

今回は(命令)の部分に、「System.out.println(...);」という「標準出力に文字列を表示する命令」を書きました。 Javaの文字列は " (ダブルクウォーテーション)で囲んで「"Hello, world!"」のように指定します。

練習

2行以上の出力

(命令)の部分には複数行の命令を書くこともできます。

public class (ファイルの名前) {

    public static void main(String[] args) {
        (命令)
    }
}

Hello2.javaという名前のファイルを作成して次の内容を書き込んでください。

public class Hello2 {

    public static void main(String[] args) {
        System.out.println("こんにちは");
        System.out.println("世界");
    }
}

先ほどと同じように実行してみましょう。

cd (ファイルを作成したフォルダ)
javac Hello2.java
java -cp . Hello2

今回は「こんにちは」と「世界」がそれぞれの行に表示されたと思います。

こんにちは
世界

このように、(命令)の部分には連続してJavaの命令を書くことができます。 今回は「System.out.println(...);」という「標準出力に文字列を表示する命令」を2回使って、2行にわたって文字列を表示しました。

System.out.println("こんにちは");
System.out.println("世界");

また、命令は上から書いた順に実行されます。 1行目は「こんにちは」という文字列を表示して、2行目は「世界」という文字列を表示するプログラムですので、その順番で表示されたと思います。

練習

おまけ

2010年5月6日木曜日

XCode上でXIBファイルを作る時の注意点

大した話でもないんですが。

UIViewControllerのサブクラスのソースファイルをXCode上で新規作成する際に、XIBファイルも同時に作成するオプションがあります。

このオプションを使って出来たXIBファイルは、ソースファイルと同じ場所に出来ます。これではなんなので、XCode上でResourcesへXIBファイルを移動したくなりまして、ドラッグ&ドロップでResources直下へ落としてやったとします。

果たしてそれはXCodeの「グループとファイル」上では上手くいくのですが、ファイルシステム上の位置は変わりません。Classesの中に居っぱなしです。

また、XIBファイルのみを新規作成する場合、「グループとファイル」上のResourcesを選択してその中に作るかと思いますが、そうするとファイルシステム上だとプロジェクトルートにXIBファイルが作成されます。

XCodeの「グループとファイル」ペイン上ではちゃんと仕分けされているので問題はないのですが、どうも気持ち悪いですし、Finderで確認してみようかなという局面の時に要らぬ混乱を来す恐れがあります。

ローカライズなど始めた場合、そこここにxxxx.lprojが出来たりしてどうも落ち着きません。

要は「グループとファイル」上でのドラッグ&ドロップによるファイル移動時にファイルシステムでの移動も可能にする方法があればよいのですが、済みません、見つかってないです。

XCodeのプロジェクトはIDE上で整理出来てればOKだから、ファイルシステム上は野放図でいいよねというポリシーなんでしょうか。

2010年4月30日金曜日

データストアのメンテナンスモードに対応する

Google App Engineのデータストアは定期的にメンテナンスモードに入ります。 メンテナンス中はデータストアが読み取り専用になって一切の書き込みが禁止されます。

Python版にはCapabilityServiceというものが用意されていてこれらを調べるのは簡単ですが、Java版にはまだ用意されていないようです。

Java版で(無理せず)これをテストするには、次のようなサーブレットフィルタを用意してやります。

package com.example;

import java.io.IOException;
import java.util.concurrent.Future;

import javax.servlet.*;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.*;

public class MaintenanceFilter implements Filter {

    @Override
    public void init(FilterConfig conf) throws ServletException {
        return;
    }

    @Override
    public void destroy() {
        return;
    }

    @Override
    public void doFilter(
            ServletRequest req,
            ServletResponse res,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Delegate<Environment> delegate = ApiProxy.getDelegate();
        try {
            ApiProxy.setDelegate(new MaintenanceDelegate(delegate));
            chain.doFilter(req, res);
        }
        finally {
            ApiProxy.setDelegate(delegate);
        }
    }

    private static class MaintenanceDelegate implements Delegate<Environment> {

        private final Delegate<Environment> delegate;

        MaintenanceDelegate(Delegate<Environment> delegate) {
            this.delegate = delegate;
        }

        @Override
        public void log(Environment env, LogRecord record) {
            this.delegate.log(env, record);
        }

        @Override
        public Future<byte[]> makeAsyncCall(
                Environment env,
                String service,
                String method,
                byte[] bytes,
                ApiConfig config) {
            return this.delegate.makeAsyncCall(env, service, method, bytes, config);
        }

        @Override
        public byte[] makeSyncCall(
                Environment env,
                String service,
                String method,
                byte[] bytes) throws ApiProxyException {
            if (service.equals("datastore_v3")) {
                if (method.equals("Put") || method.equals("Delete")) {
                    throw new CapabilityDisabledException(service, method);
                }
            }
            return this.delegate.makeSyncCall(env, service, method, bytes);
        }
    }
}

これをweb.xmlに登録します。このとき、「/_ah/」以下をフィルタするといろいろ不具合が発生するので、必要最小限にしましょう。

<filter>
    <filter-name>MaintenanceFilter</filter-name>
    <filter-class>com.gluegent.goose.soc.front.util.MaintenanceFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>MaintenanceFilter</filter-name>
    <url-pattern>/gadget/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

このフィルタを経由した状態でデータストアに書き込みを行うと、CapabilityDisabledExceptionがスローされるようになります。 例外をキャッチしてメンテナンス中の処理を書いておきましょう。

try {
    // データストアへの書き込み
}
catch (CapabilityDisabledException e) {
    // メンテナンス中の処理
}

メンテナンスモードのときの動作がエミュレーションされるので、この状態で様々なテストを実施します。 なお、このフィルタを残したままサービスインするとひどいことになります。

追記

ご指摘いただきました。 メンテナンスモードのときはDatastoreService.allocateIds()やMemcacheなども動かなくなるので、makeSyncCallメソッドの中でもう少しトラップが必要です。 具体的にはこんな感じ:

@Override
public byte[] makeSyncCall(
        Environment env,
        String service,
        String method,
        byte[] bytes) throws ApiProxyException {
    if (service.equals("datastore_v3")) {
        if (method.equals("Put") ||
                method.equals("Delete") ||
                method.equals("AllocateIds")) {
            throw new CapabilityDisabledException(service, method);
        }
    }
    else if (service.equals("memcache")) {
        throw new CapabilityDisabledException(service, method);
    }
    return this.delegate.makeSyncCall(env, service, method, bytes);
}

2010年4月27日火曜日

カメラ、フォトライブラリを表示する

カメラ、フォトライブラリを表示、利用するにはUIImagePickerControllerを利用する。UIImagePickerControllerSourceTypeというenumがあるのでこれを利用してカメラ、フォトライブラリなどのいずれを利用するか決める。

カメラを表示するソース。delegeteに自身を渡す場合はUIImagePickerControllerDelegate, UINavigationControllerDelegateの両方を実装していなければならない。

// 有効かどうかを確認する
if (![UIImagePickerController 
      isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {  
    return;
}
    
UIImagePickerController *cameraController = 
        [[[UIImagePickerController alloc] init] autorelease];
cameraController.sourceType = UIImagePickerControllerSourceTypeCamera;
cameraController.allowsImageEditing = NO;
cameraController.delegate = self;
[self presentModalViewController:cameraController animated:YES];

2010年4月26日月曜日

App Engine SDK 1.3.3.1 リリース (?)

先日にGAEのSDK 1.3.3がリリースされましたが、今回はJava版のバグフィックスのようです。(Python版は出ていません)

  • http://code.google.com/p/googleappengine/issues/detail?id=3138

SDK 1.3.3のServlet Filterでワイルドカードを指定した際にうまく動作しなかったバグを修正したとのことです。

ダウンロードはプロジェクトのDownloadsのページから。

ただ、日本時間で2010/04/26 10:31の時点では、上記のIssueが完了していなかったり、リリースノートのページにリリースが出ていなかったりでどういう扱いなのかちょっと不明です。

追加情報があれば改めてお知らせします。

2010年4月23日金曜日

UITableViewCellの現在の位置を取りたい

UITableViewCellの現在表示されている位置を取得しよう、というんで素直に

CGRect rect = cell.frame;

と取ってみたくなりますが、スクロールが行われている場合はこれでは取れないです。このrectの持っている座標は、スクロールが行われてない場合の位置、つまり画面の外を指しています。

なので、どれだけUITableViewがスクロールされているかを取って引き算してやればいい、と。UITableViewにcontentOffsetなんてプロパティがあります。

CGPoint offset =  tableView.contentOffset;
rect.origin.x = rect.origin.x - offset.x;
rect.origin.y = rect.origin.y - offset.y;

めでたきかなこれで取れました。

autoreleaseのタイミング

前回のわたくしのNSIndexPathについてのポストで、autoreleaseだとメモリ圧迫しちゃうねぇなどと書きましたが。

Xcodeで自動で生成されるソースを見ると、main.mに

int main(int argc, char *argv[])
{

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  int retVal = UIApplicationMain(argc, argv, nil, nil);
  [pool release];
  return retVal;
}

などとありまして、ああautoreleaseにした奴はアプリケーションが終了するまでプールされっぱなしなのだな、これは気をつけなきゃいけないなと思っていたのですが。

敢えてautoreleaseにしてたオブジェクトが予期せぬタイミングで初期化されていてエラーで落ちるという現象が起きてびっくり。調べてみたら全く同様の事を記事にしてらっしゃる方がいました。

http://insideflag.blogspot.com/2009/08/autorelease.html

どうやらUIKitでは、イベント発生でプールが作られて終了でreleaseされるようです・・・。なんてこった。いや楽になってよかった。

App EngineのBlobstoreにアップロードしたデータを解析する

Blobstoreのデータをアプリケーションで利用するに引き続き、どんな感じでGAEのBlobstoreにアップロードしたデータを解析しているか紹介。

とりあえず、下記のような形でPOSTとPUTをハンドルするサーブレットを作ってます。 POSTはBlobstoreにアップロードした後のリダイレクト先で、PUTはさらにそれを解析する部分です。

...
public class BlobAnalyzeServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private static final String K_BLOB_KEY = "k";

    private transient BlobstoreService blobstore;

    @Override
    public void init() throws ServletException {
        readResolve();
    }

    private Object readResolve() {
        blobstore = BlobstoreServiceFactory.getBlobstoreService();
        return this;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        Map<String, BlobKey> blobs = blobstore.getUploadedBlobs(req);
        if (blobs.size() != 1) { // この例では同時に1つだけアップロード
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        BlobKey key = blobs.values().iterator().next();

        // TaskQueueを利用して裏側でdoPutを実行
        QueueFactory.getDefaultQueue().add(
            TaskOptions.Builder
                .url(req.getServletPath())
                .method(TaskOptions.Method.PUT)
                .param(K_BLOB_KEY, key.getKeyString()));

        resp.sendRedirect("/<成功した際のリダイレクト先>");
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String keyString = req.getParameter(K_BLOB_KEY);
        BlobKey blobKey = new BlobKey(keyString);
        InputStream input = new BlobInputStream(blobKey, 512 * 1024);

        // 以降、blobの内容を解析
    }
}

なお、doPutの中で使っているBlobInputStreamは、前回紹介したプログラムと同じです。第2引数の「512 * 1024」は一度にフェッチするサイズで、最大の1MBの半分を指定しています。 アップロードされたファイルの内容を直に取れるので、ファイルの内容を解析したりできます。

TaskQueueを利用している関係で、アップロードしてから解析が終わるまでに少し遅延がありますが、解析を別プロセスで行うためリクエストを速くさばけるという利点があります。

Blobstore自体の使い方は「AppEngine1.3.0 Blobstore API入門」辺りを参考にしてください。

2010年4月22日木曜日

Google App Engine SDK 1.3.3 リリース

GAEのSDK 1.3.3がリリースされました。リリースノートはこちら

プレリリースの内容がすべて盛り込まれたわけでもない感じ(データストアのID自動採番時に範囲を指定できるメソッドが入っていない)で、いくつかのバグフィックスや細かい修正が主です。いくつかダイジェストで紹介します。

  • アプリケーションIDとバージョンを確認するシステムプロパティの追加

System.getProperty()メソッドでデプロイした際のアプリケーションIDとバージョンをアプリケーションから取得できるようになりました。

プロパティ名を覚えるのが大変なので、SystemPropertyというクラスを使うといいと思います。

import com.google.appengine.api.utils.SystemProperty;
...

String id = SystemProperty.applicationId.get();
String version = SystemProperty.applicationVersion.get();
  • 30秒制限の前に確実にキャッチできるDeadlineExceededExceptionがスローされるようになった

これがスローされなかったことは今まで見たことがないですが、確実にスローされるようにした、というところでしょうか。

  • QuotaService.getCpuTimeInMegaCycles()の修正

これまでは「InMegaCycles」と言いながらメガじゃないCPUサイクル数を返していたようです。

  • トランザクション時に一部の例外を識別できるようになった (?)

CommittedButStillApplyingExceptionという例外が増えています。

App Engineのデータストアでトランザクションを利用してエンティティを書きこむと、Life of a Datastore Writeにある通りCommit, Applyという2つのフェーズが順に行われます。このうち、Applyでエラーが起こった場合にはエンティティは書き込まれているもののインデックスが不整合の状態になります。

これまではCommitで失敗してもApplyで失敗しても同じエラーが返されていましたが、この例外がスローされた場合はエンティティの書き込み自体は成功していることが分かります。インデックスを反映させる(Apply)には、そのエンティティをトランザクション内で読みに行くだけで自動的にロールフォワードという処理が行われます。

この例外がスローされる状況をまだ再現するほど実験していないので、追加情報があれば後日お知らせします。

Java版の更新はだいたい以上です。なお、Python版のSDKは、開発サーバ上のデータストアにバックエンドとしてSQLiteを指定できるようになったようです。

2010年4月21日水曜日

ModalViewの表示時のアニメーションを変更する

UIViewControllerのプロパティmodalTransitionStyleを変更することで可能。

以下の3種類用意されている。

  • UIModalTransitionStyleCoverVertical
    • 下から上にせり出してくる。デフォルトはこれ。
  • UIModalTransitionStyleCrossDissolve
    • 画面がフェードアウト、フェードインする
  • UIModalTransitionStyleFlipHorizontal
    • 水平方向に画面がくるっと回る(フリップする)

デフォルトのアニメーションを使いつつ、キーボードを表示した状態で画面を開くと、キーボードがあたかも画面に組み込まれてるパーツのように見せることが可能ということも分かった。キーボードを最初から表示させる為には次のようにすればいい。

@implementation ModalViewController
@synthesize textView;
-(void) viewDidLoad {
    // 最初のresponderに設定することで自動的にキーボードが表示される。
    [textView becomeFirstResponder];
    [super viewDidLoad];
}
@end

ModalViewの表示に関しては先程のエントリを参照。

自前でNSIndexPathを生成する

UITableViewで、現在の行の次の行や前の行を参照したい場合、NSIndexPathを自分で生成しないといけないのだろうなと思いつくわけですが、さてどうしたもんか。

調べると、こうすればよいという情報がそこここにあります。

NSIndexPath newPath = [NSIndexPath indexPathForRow:3 inSection:1];

この場合、2番目のセクションの4行目を表すNSIndexPath が出来ます。出来ますが。

indexなどと始まる名前のクラスメソッドの常で、これはファクトリクラスメソッドで、生成するインスタンスに対して内部でautoreleaseしてます。イベントなどでなんども発生するようなメソッド内で書くと、あっという間にメモリを食ってしまいます。なのでちゃんと自分でオーナーシップを握るようにしないといけません。

そもそもNSIndexPathって何だろうって話なのですが、一番判りやすい説明は「アウトライン上の位置情報」でした。

  • 1章
    • 1-1節
    • 1-2節
      • 1-2-3項
    • 1-3節
  • 2章
    • 2-1節 #いまここ
    • 2-2節

以下略

このいまここ、がNSIndexPath。階層は任意で決められます。UITableViewのDataSourceやDelegateのメソッドで扱ってるものは、UITableView用にカテゴリで拡張したもので、階層は2階層で固定、それぞれを表すsectionとrowというプロパティが追加されています。

なので自前で作るときはこうなります。

NSUInteger newIndex[] = {1, 3}; // 2番目のセクションの4行目
NSIndexPath newPath = [NSIndexPath alloc] initWithIndexes:newIndex length:2];
//ああだこうだの処理
[newPath release];

length: が階層ですね。これで自分でお掃除も出来ます。どうせならUITableView用に特化したイニシャライザも作ってくれればよかったのに。上の処理をラップすりゃ出来るんだし。

ModalViewの表示

今いる画面で一時的に表示したい(イメージ的には遷移を伴わない画面の表示)時に使う方法。

表示する時

// ModalViewControllerはUIViewControllerのサブクラス
ModalViewController *modalViewController = [[ModalViewController alloc]
                       initWithNibName:@"ModalView" bundle:nil];
[self presentModalViewController:modalViewController animated:YES];
[modalViewController release];

閉じる時

// ModalViewController側に書く
[self dismissModalViewControllerAnimated:YES];

自分のアプリをリンクで起動させる方法

iPhoneではサファリ(おそらくその他のブラウザからでも)から電話、SMS、Youtubeなどに対するリンクを自動的に判別し、 個々のアプリを開いてくれるという機能がありますが、せっかくのこの機能自分のアプリでも使いたい!と思って調べてみました。

結果以下手順で、サファリからリンクを入力して起動することが出来ました。

まず、info.plistに以下を追記。ただし、これはXCodeからいじるとkeyの名前が若干違って見えるので注意です。

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.gluegent.showAlert</string>
    <key>CFBundleURLSchemes</key>
    <array>
        <string>launchtest</string>
    </array>
  </dict>
</array>

ApplicationDelegateに以下のメソッドを追記。

-(BOOL) application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    // どこからでも呼ばれる可能性があるので
    // スキーマ名、パスなどきちんと確認する必要がある。
    if ([[url scheme] isEqualToString:@"launchtest"]) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"LaunchTest"
                              message:@"Hey, Success!!"
                              delegate:nil
                              cancelButtonTitle:nil
                              otherButtonTitles:@"Yappie!!", nil];
        [alert show];
        [alert release];
        return YES;
    }
    return NO;
}

最後にアプリをシミュレータで起動したあとで、サファリからURL入力欄にlaunchtest:com.gluegent.showAlertと入力するとアラートが表示されます。この情報は意外とたどり着けなかったんですが、普通に公式のTodoアプリのチュートリアルの中でも解説されていました。(リンクを失念。。。)

ただし、このリンクによる起動は注意しないと重大なセキュリテリィホールを作ってしまう危険性もあります。 詳しくはApple公式のScureCodingGuideを参考にしてみてください。

Blogger APIを利用してエントリを投稿する

Google Bloggerにエントリを投稿するツールを作っていました。この辺からGData関連のライブラリをダウンロードして使います。正式なドキュメントはBlogger APIにあります。

まず、投稿するエントリを作成する部分。

String CATEGORY_SCHEME = "http://www.blogger.com/atom/ns#";
String title = ...; // 投稿タイトル
String content = ...; // 内容 (HTML)
List<String> tags = ...; // タグ

Entry entry = new Entry();
entry.setTitle(new PlainTextConstruct(title));
entry.setContent(new HtmlTextConstruct(content));
for (String tag : tags) {
    entry.getCategories().add(new Category(CATEGORY_SCHEME, tag));
}

AuthSubで認証してエントリを投稿する部分。他にも認証形式はいくつかありますが、身内で使うツールだったらこれくらいでよいかと思います。

// 投稿画面のURLにblogID=... と書いてある数字
String blogId = ...;
// 投稿ツールのID
String toolId = "<company>-<tool>-<version>";
String user = ...; // ユーザ名
String password = ...; // パスワード

BloggerService service = new BloggerService(toolId);
service.useSsl();
service.setUserCredentials(
    user,
    password,
    ClientLoginAccountType.GOOGLE);
URL postUrl = new URL("http://www.blogger.com/feeds/" +
        blogId +
        "/posts/default");
Entry post = service.insert(postUrl, entry);

// 投降したエントリのURL
String result = post.getHtmlLink().getHref();

一点はまったのが、setUserCredentialsで"ClientLoginAccountType.GOOGLE"を指定する部分でした。 Google Appsと同じメールアドレスでGoogleアカウントを持っていると、通常では認証がGoogle Appsの方で行われてしまいます。 これを指定するとGoogle Appsでの認証を行わないので、普通に運用しているbloggerだったらこれを指定しておく方が無難です。

2010年4月16日金曜日

ネストしてるNSDictionaryをドット参照で取得する

NSDictionaryがネストされているケースなどの場合いちいち取得してキャストしてといったようなコードを書かないといけないのかと思ってましたが一発で抜けるメソッドがありました。これは便利!

NSDictionary* nested = ...;
// わざわざこう書かなくても
NSString toughName = [(NSDictionary*)[nested objectForKey:@"user"] objectForKey:@"name"];
// こう書ける
NSString easyName = [nested valueForKeyPath:@"user.name"];

2010年4月9日金曜日

Blobstoreのデータをアプリケーションで利用する

Google App Engine SDK 1.3.2からはBlobstoreService APIにfetchDataというメソッドが追加されています。 これまではBlobstoreにデータをアップロードしても、アプリケーションの中から中身を見ることができなかったのですが、これを使うとそれができるようになる様子です。

ただしこのメソッド、1回に1MBずつしか転送できなかったりして面倒なので、InputStreamでラップしてみました。軽く実験したところ10MBのファイルを1秒程度で読み出せたりするなど、妙に優秀なので何か間違っているんじゃないかと不安になってます。

以下、ラップしたプログラムです。+expandとかで全部見えると思います。

package com.example;

import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.appengine.api.blobstore.BlobInfo;
import com.google.appengine.api.blobstore.BlobInfoFactory;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;

/**
* Blobの中身を読み出すストリーム。
*/
public class BlobInputStream extends InputStream {

private static final Logger LOG = LoggerFactory.getLogger(
    BlobInputStream.class);

private transient BlobstoreService service;

private final BlobInfo info;

private final int chunkSize;

private long positionInBlob;

private transient int positionInChunk;

private transient byte[] chunk;

private boolean closed;

/**
 * インスタンスを生成する。
 * @param key 読みだす対象のBlobへのキー
 * @param chunkSize 分割して読みだす個々のチャンクのバイト数
 * @throws IOException Blobが存在しない場合や、Blobのメタ情報取得に失敗した場合
 * @throws IllegalArgumentException 引数に{@code null}が含まれる場合、
 *     または{@code chunkSize}に0以下の値が指定された場合
 */
public BlobInputStream(BlobKey key, int chunkSize) throws IOException {
    if (key == null) {
        throw new IllegalArgumentException("key is null"); //$NON-NLS-1$
    }
    if (chunkSize <= 0) {
        throw new IllegalArgumentException("chunkSize <= 0"); //$NON-NLS-1$
    }
    this.service = BlobstoreServiceFactory.getBlobstoreService();
    this.info = new BlobInfoFactory().loadBlobInfo(key);
    if (info == null) {
        throw new IOException(MessageFormat.format(
            "BlobInfo({0}) is not found",
            key.toString()));
    }
    this.chunkSize = Math.min(chunkSize, BlobstoreService.MAX_BLOB_FETCH_SIZE);
    this.positionInBlob = 0;
    this.positionInChunk = 0;
    this.chunk = null;
    this.closed = false;
}

/**
 * インスタンスを生成する。
 * @param info 読みだす対象のBlobメタ情報
 * @param chunkSize 分割して読みだす個々のチャンクのバイト数
 * @throws IllegalArgumentException 引数に{@code null}が含まれる場合、
 *     または{@code chunkSize}に0以下の値が指定された場合
 */
public BlobInputStream(BlobInfo info, int chunkSize) {
    if (info == null) {
        throw new IllegalArgumentException("info is null"); //$NON-NLS-1$
    }
    if (chunkSize <= 0) {
        throw new IllegalArgumentException("chunkSize <= 0"); //$NON-NLS-1$
    }
    this.service = BlobstoreServiceFactory.getBlobstoreService();
    this.info = info;
    this.chunkSize = Math.min(chunkSize, BlobstoreService.MAX_BLOB_FETCH_SIZE);
    this.positionInBlob = 0;
    this.positionInChunk = 0;
    this.chunk = null;
    this.closed = false;
}

/**
 * インスタンスを生成する。
 * @param service 実際にBlobの内容を読み出すサービス
 * @param info 読みだす対象のBlobメタ情報
 * @param chunkSize 分割して読みだす個々のチャンクのバイト数
 * @throws IllegalArgumentException 引数に{@code null}が含まれる場合、
 *     または{@code chunkSize}に0以下の値が指定された場合
 */
BlobInputStream(BlobstoreService service, BlobInfo info, int chunkSize) {
    if (info == null) {
        throw new IllegalArgumentException("info is null"); //$NON-NLS-1$
    }
    if (chunkSize <= 0) {
        throw new IllegalArgumentException("chunkSize <= 0"); //$NON-NLS-1$
    }
    this.service = service;
    this.info = info;
    this.chunkSize = Math.min(chunkSize, BlobstoreService.MAX_BLOB_FETCH_SIZE);
    this.positionInBlob = 0;
    this.positionInChunk = 0;
    this.chunk = null;
    this.closed = false;
}

@Override
public int read() throws IOException {
    checkAvailable();
    if (restInBlob() == 0) {
        return -1;
    }
    prepareChunk();
    int c = chunk[positionInChunk] & 0xff;
    advance(1);
    return c;
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
    checkAvailable();
    if (b == null) {
        throw new IllegalArgumentException("b is null"); //$NON-NLS-1$
    }
    if (len == 0) {
        return 0;
    }
    if (restInBlob() == 0) {
        return -1;
    }
    int read = (int) Math.min(restInBlob(), len);
    int cursor = off;
    int rest = read;
    while (rest > 0) {
        assert restInBlob() > 0;
        prepareChunk();
        int readInChunk = Math.min(rest, restInChunk());
        System.arraycopy(
            chunk, positionInChunk,
            b, cursor,
            readInChunk);
        advance(readInChunk);
        rest -= readInChunk;
        cursor += readInChunk;
    }
    assert rest == 0;
    return read;
}

@Override
public int available() throws IOException {
    checkAvailable();
    return restInChunk();
}

@Override
public long skip(long n) throws IOException {
    checkAvailable();
    if (n < 0) {
        throw new IllegalArgumentException();
    }
    if (n == 0) {
        return 0;
    }
    long skip = Math.min(n, restInBlob());
    advance(skip);
    return skip;
}

@Override
public void close() throws IOException {
    chunk = null;
    closed = true;
}

private void advance(long count) {
    assert count <= restInBlob();
    positionInBlob += count;
    if (chunk != null) {
        if (count < restInChunk()) {
            positionInChunk += (int) count;
        }
        else {
            chunk = null;
            positionInChunk = 0;
        }
    }
}

private void prepareChunk() throws IOException {
    assert restInBlob() > 0;
    if (chunk != null) {
        assert positionInChunk < chunk.length;
        return;
    }
    long endIndex = Math.min(info.getSize(), positionInBlob + chunkSize);
    LOG.debug("Preparing chunk: name={}, size={}, range=[{}, {})", new Object[] {
            info.getFilename(),
            info.getSize(),
            positionInBlob,
            endIndex
    });
    try {
        chunk = service.fetchData(
            info.getBlobKey(),
            positionInBlob, endIndex);
        positionInChunk = 0;
    }
    catch (Exception e) {
        throw new IOException(e);
    }
    assert chunk.length > 0 : positionInBlob;
}

private int restInChunk() {
    if (chunk == null) {
        return 0;
    }
    return chunk.length - positionInChunk;
}

private long restInBlob() {
    return info.getSize() - positionInBlob;
}

private void checkAvailable() throws IOException {
    if (closed) {
        throw new IOException(MessageFormat.format(
            "{0} for {1} is already closed",
            getClass().getSimpleName(),
            info.getBlobKey()));
    }
}
}
追記 (2011.01.14 KATO)
1.3.5以降からBlobstoreInputStreamが提供されていますので、そちらをお使いください。

2010年4月8日木曜日

XCodeのプロジェクト構成

まずXCodeによる開発を行った場合のプロジェクト構成をまとめてみる。XCodeではプロジェクトの構成は必ずしも実ディレクトリと一致していない。XCode上で見える構成はグループという実ファイルに対する参照で取り扱われてる。とりあえずグループ:実ディレクトリの構成で書いてみる。

Classes:${basedir}/Classes/${projectName}
ここには作成するアプリのコードを置く。Delegateクラス、ViewControllerクラスなどなど。
Other Sources:${basedir}
ブートストラップとなるmain.m、共通のヘッダファイルとなる.pchなどはここに置く。CやC++のソースもここに置くのが慣習らしい。アプリケーション全体に関連するものを置く場所なのかな。
Resources:${basedir}
名前の通りリソースファイルを置くところ。画像、音声データや国際化対応用のファイルなどなど。mavenでいうとろこの"src/main/resources"。
Frameworks:実体は無し
アプリが利用するフレームワークが置かれる。EclipseでいうところのJRE_CONTAINERとかMAVEN2_CLASSPATH_CONTAINER。
Products:${basedir}/build
ビルド成果物が置かれる。mavenでいうところのtargetディレクトリ。

この構成の各グループの意味についてはiPhone SDK アプリケーション開発ガイドで知ることが出来た。

2010年3月26日金曜日

SDK 1.3.2です

ここを参照。 それにしても"we are excited to announce ..."っていうのは定型句なんでしょうか。ニュースサイトで直訳されてるの見て、なんだか変だなといつも思います。

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トランザクションを利用していた設計よりも多くのことを考えなければなりません。 このエントリで紹介した方法は、パフォーマンスに悪影響を及ぼす可能性が考えられますので、必要な整合性や設計および実装の手間を考慮しながら、注意深く選択することが必要になります。