Hatena::ブログ(Diary)

やさしいデスマーチ このページをアンテナに追加 RSSフィード

札幌のWebエンジニアの綴る日常と開発の日々。
GoogleAppEngine/slim3/Django/NetBeans/Swing/JavaFXを中心にお届け。

2010-05-30

Kotanの開発状況

| 22:02 |  Kotanの開発状況を含むブックマーク

最近は色々と平行で進めていて進捗が悪いのですが、ここ2−3日はKotanの開発を進めていました。KotanはAppEngine上のデータを引き抜いて表示・編集のできるSwingアプリケーションです。

用途としては大きく2つあり、1つはローカル環境でのデータ作成、主にテストデータを簡単に作成したいという所から開発を始めています。仕組みとしては、開発サーバを別プロセスとして起動し、ローカルでHttp通信を行って取得・更新を行います。

2つ目の用途としては本番環境のデータを引き抜いて、ローカル環境のDatastoreにコピーすることです。これもよくやりたいパターンですが、AppEngineのJavaではサポートされていないこともあり、作っている状況です。本番環境へのアクセスは組込モードと同様にHttp通信で行い、ローカルアプリとして起動して通信する方法とJavaWebStartとしてサーバに設置してしまう方法の2つの使い方を想定しています*1

まだドキュメントはほとんどありませんが、 デモサイトプロジェクトサイトを置いておきます。

*1:JavaWebStartにする方法はetupirkacmsではCMSのサイトデータをローカル環境にバックアップしたり、逆に復元したりする所でKotanをライブラリとして使う予定

2010-05-24

64bitのJVMでslim3を使った開発をする方法

| 00:09 |  64bitのJVMでslim3を使った開発をする方法を含むブックマーク

正式には32bitしかサポートされていません。とはいえ、Javaの設定でいちいち切り替えるのも面倒です。しかし、MacJavaには-d32という特別なオプションがあり、これを指定すれば32bitで動作してくれます。

以下設定。

Eclipse - 環境設定 - Java - Installed JRE's を開く。

f:id:shuji_w6e:20100524000802p:image

Addで環境を追加する。

JRE HOMEにはデフォルトのHomeと同じパス(/System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home)、JRE nameには「JVM 1.6(32bit)」とでも入力、Default VM Argumentsに「-d32 」を指定。

f:id:shuji_w6e:20100524000803p:image

後はデフォルトVMとして32bitにしてしまうか、slim3のプロジェクトでビルドパスの指定からJREを32bitにしてください。ただし、システムのデフォルトJREにせず、SVNなどでプロジェクトを共有する場合は注意しましょう(相手の環境にも同じ名前で作る必要有り)。

2010-05-23

sessionを有効にするとspin-upは遅くなるか?

| 22:35 |  sessionを有効にするとspin-upは遅くなるか?を含むブックマーク

計測していなかったので改めて計測してみました。

尚、ここで「有効にする」というのは、appengine-web.xmlのsessions-enabledをtrueにする事を指し、最初のリクエストでsessionのAPIを触らないケースを想定しています(Sessionを使用すれば当然影響はありますが、それは後ほど検証)。

テストはetupirkacmsトップページの表示です。

sessions-enabled=false

/ 200 1458ms 1738cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1475ms 1796cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1477ms 1796cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1478ms 1796cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1385ms 1680cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1437ms 1719cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1516ms 1796cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1578ms 1875cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1551ms 1796cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 2053ms 1777cpu_ms 8api_cpu_ms 3kb gzip(gfe)

sessions-enabled=true

/ 200 1498ms 1796cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1699ms 1816cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1528ms 1796cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1795ms 1816cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1564ms 1777cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 2027ms 1700cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1701ms 1835cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1603ms 1758cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1734ms 1816cpu_ms 8api_cpu_ms 3kb gzip(gfe)

/ 200 1391ms 1700cpu_ms 8api_cpu_ms 3kb gzip(gfe)

考察

誤差の範囲じゃないでしょうか?Sessionを有効にしたとしても、適切なタイミングで使用するのであればspin-upへの影響はないと考えてよさそうです。

2010-05-20

dev_appserverを外部プロセスとして起動する方法と殺す方法

| 00:10 |  dev_appserverを外部プロセスとして起動する方法と殺す方法を含むブックマーク

Kotanの実行環境としては3パターンを想定しています。

- プロダクション環境(Java Web Startで実行)

- ローカルサーバ環境(Java Web StartまたはJavaアプリケーション

- 組込AppEngine環境(Javaアプリケーション

どれも、サーバとはHttpでデータを通信していますが、都度サーバを動かす2つ目の方法はちょっと面倒です。

そこで3つ目の実行方法として、スタンドアロンアプリケーションとしてKotanを動かします。組込モードでは、Kotanが外部プロセスとしてAppEngineのローカルサーバを起動します。この時、ApplicationIdやDatastoreのパスを指定する事ができるため、Datastoreのバイナリファイルを好きなように作成することができるわけです。

f:id:shuji_w6e:20100521000343p:image

この機能により、開発中のアプリケーションのデータをコピーしてKotanで編集すること、Kotanで作ったデータを開発中のアプリケーションのテストデータとして使うことなどができるわけです。

起動オプションあれこれ

Datasroeを指定して起動する

    • jvm_flag=-Ddatastore.backing_store=MY_STORE

ApplicationIdを指定して起動する

    • jvm_flag=-Dcom.google.appengine.application.id=MY_APPLICATION_ID

ApplicationのVersionを指定して起動する

    • jvm_flag=-Dcom.google.appengine.application.version=MY_VERSION

尚、dev_appserver.shなどのスクリプトを使って起動してしまうと、Process#destoroyでサーバが殺せません。dev_appserver.shを参考にして、Javaコマンドでサーバを起動します。

参考:http://code.google.com/p/kotan/source/browse/trunk/kotan/src/main/java/kotan/server/embedded/DevAppServerProcess.java

2010-05-17

Datastoreのデータを表示・編集したい

| 23:37 |  Datastoreのデータを表示・編集したいを含むブックマーク

GoogleAppEngineで開発をしている時にDatastoreのデータを直接扱いたい場合、管理コンソールを使えばだいたいの事は可能です。ですが、Keyの編集やBlobデータのアップロードなど細かい事は出来ませんし、ウェブインターフェイス故に限界もあります。また、テストデータを作りたい場合などは、Excelのようにコピーをしながら簡単に作り、保存なんかもしたいわけです。

そこで新しいプロジェクトKotanを作ってみました。スクリーンショットはこんな感じ。

f:id:shuji_w6e:20100517233507p:image

デモサイトはこちら

ただし、Java Web Startで起動し嘘証明書を許可しなければ使えません。また、データアクセスをする為にGoogle Accountによる認証も必要になります。

技術的には、単純なSwingクライアントアプリです。Commons Http Clientを使いサーバと通信を行いますが、GoogleのAccountの認証が少々手間です。サーバ側はslim3を使い、指定されたEntityをシリアライズ化してやりとりする単純仕様です。ソースはたいした量ではないので、興味のある方はこちらからどうぞ。

現時点では、まだ作りかけ(2日しかかけてませんw)なのですが、今後は投入したデータからテスト用のデータ生成コードをはき出す機能や、ローカルバックアップ機能、Excel連動機能など、ウェブベースだけでは作れない(作りにくい)機能を追加していきたいと思っています。

2010-05-10

単体テストで ImagesService#applyTransform を使う方法

| 21:27 |  単体テストで ImagesService#applyTransform を使う方法を含むブックマーク

slim3ではGAEの環境に依存するような単体テストもかなり簡単に行う事ができます。例えば、Bigtableへのアクセスはテストの実行毎にリセットされる、グローバルトランザクションに対応など至れりつくせりです。

ところが、画像を加工してサムネイルを作る時などに使用する ImagesServiceですが、これを単体テストで実行しようとすると例外が発生します。内部的には、画像の作成や一部の軽い処理はImplで行っているようですが、拡大縮小などの変換処理は各アプリでやるのではなく、外部サービスとして呼び出しているようです。これらのテスト環境でのエミュレートは、org.slim3.tester.AppEngineTesterのmakeSyncCallで行っていますので、ここを少しカスタマイズすることでImagesServiceもエミュレートできるようになります。

以下、簡単なサンプルになります。

public class MyTester extends org.slim3.tester.ControllerTester {
    private static final String IMAGE_SERVICE = "images";
    public final ImageServiceStub imageServiceStub = new ImageServiceStub();

    @Override
    public byte[] makeSyncCall(Environment env, String service, String method, byte[] requestBuf)
            throws ApiProxyException {
        if (service.equals(IMAGE_SERVICE)) {
            Queue<byte[]> queue = imageServiceStub.imageStore.get(method);
            if (queue == null || queue.isEmpty()) throw new ApiProxyException("service=" + service + ", method=" + method);
            ImagesServicePb.ImagesTransformResponse res =
                    new ImagesServicePb.ImagesTransformResponse();
            ImagesServicePb.ImageData data = new ImagesServicePb.ImageData();
            data.setContentAsBytes(queue.peek());
            res.setImage(data);
            return res.toByteArray();
        }
        return super.makeSyncCall(env, service, method, requestBuf);
    }
    
    public class ImageServiceStub {
        Map<String, Queue<byte[]>> imageStore = new HashMap<String, Queue<byte[]>>();
        public void register(String method, byte[] imageData) {
            Queue<byte[]> queue = imageStore.get(method);
            if (queue == null) {
                queue = new LinkedList<byte[]>();
                imageStore.put(method, queue);
            }
            queue.add(imageData);
        }
    }
}

makeSyncCallはサービス名と実行メソッド名を引数に呼び出されます。ImageServiceの場合imagesなので、それをヒントに処理を上書きしましょう。requestBufは引数の情報などが格納されているので、必要な場合、適切なオブジェクトに戻します(ここではImagesServicePb.ImagesTransformRequest)。同様に戻り値もbyteに変換しますが、同様にImagesServicePb.ImagesTransformResponseを作成し、toByteArrayでbyteに戻してから返します。

後は変換処理のイメージについて、適当に登録できるようにし、メソッドの呼び出し順に返すようにしただけです。

実際にテストする場合はこんな感じ。

    @Test
    public void upload_post() throws Exception {
        // ImageServiceで返却される画像を登録
        tester.imageServiceStub.register("Transform", BinLoader.load("thumbnail.jpg"));
        FileItem item = new FileItem("sample.jpg", "image/jpg", BinLoader.load("sample.jpg"));
        tester.request.setMethod("post");
        tester.request.setAttribute("file", item);
        tester.start("/_mng/media/upload");
        assertThat(tester.response.getStatus(), is(HttpServletResponse.SC_MOVED_TEMPORARILY));
        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/_mng/media/"));
    }

この辺は、 @shin1ogawa さんのエントリーが非常に参考になりますので、あわせて参照ください。