2009年9月30日水曜日

InvocationTargetExceptionのその後

以前書いたInvocationTargetExceptionがよく起こる点について、GAE/JのGoogle Groupsを見ていたら、同じようなことで悩んでるスレッド発見。

Startup takes forever - Google App Engine for Java | Google Groups
new stack traces after upgrading to SDK 1.2.5 - Google App Engine for Java | Google Groups

後者の方で言われているが、スレッドがサポートされてないので、やっぱりこの例外はどうしようもないのかもしれないし、クリティカルなものではないからINFOレベルなわけだし。

ただそのとき、CPU時間がグッとあがる点がね・・・。ユーザー数やリクエスト数が増えたときにすぐに無料の範囲を超えないかって気になる。

2009年9月18日金曜日

JDOでListなどのコレクションを取得する時の注意点

Listのプロパティを取得しようとしたら以下のような例外が出た。
javax.jdo.JDODetachedFieldAccessException: You have just attempted to access field "tagNameList" yet this field was not detached when you detached the object. Either dont access this field, or detach it when detaching the object.
 at jp.co.tricell.sonicboom.Mutter.jdoGettagNameList(Mutter.java)
 at jp.co.tricell.sonicboom.Mutter.getTagKeys(Mutter.java:137)
 at jp.co.tricell.sonicboom.rpc.test.TestMutterUtils.testGetMutterOf(TestMutterUtils.java:69)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at junit.framework.TestCase.runTest(TestCase.java:164)
 at junit.framework.TestCase.runBare(TestCase.java:130)
 at junit.framework.TestResult$1.protect(TestResult.java:106)
 at junit.framework.TestResult.runProtected(TestResult.java:124)
 at junit.framework.TestResult.run(TestResult.java:109)
 at junit.framework.TestCase.run(TestCase.java:120)
 at junit.framework.TestSuite.runTest(TestSuite.java:230)
 at junit.framework.TestSuite.run(TestSuite.java:225)
 at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:130)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
先日購入した「Google App Engine for Java[実践]クラウドシステム構築」P124に書いてった、
「JDOにはフェッチグループという概念があり、単一のエンティティ内でも一部のプロパティを遅延して取得できます。」
と言うことの意味がこれで分かった。

プリミティブ型、そのラッパークラス、String、Dateといった基本的な型がdefault fetch groupであり、エンティティのプロパティとして「遅延無く」取得できるものなのだ。

今回使用したListはこれらに含まれていないので、自分でdefault fetch groupの指定を追加しない限り、エンティティ取得当初はデータが含まれていないということなんだな。

ということで、例外が出たプロパティに以下のアノテーションを追加。
@Persistent(defaultFetchGroup = "true")
private List<String> tagNameList;
取得成功っと。

さよならウメ子

小田原動物園の象のウメ子が天国へ旅立ってしまった。
東京新聞:突然の悲報、市民ら涙 国内最高齢ゾウ『ウメ子』天国へ 小田原:神奈川(TOKYO Web)

去年小田原に行った時のウメ子。
from Odawara Castle 2008.12.07

初めて小田原城に行った時は正直、なぜ城と象?という感じで、お城のすぐ下に動物園があるっていう光景は少し不思議だった。

でも、ご城下になぜか象がいて、猿がいて、子供たちが遊んでてっていう光景は、北条さんもちょっとびっくりだけどなんだか楽しくていいかもって思ってあるかもよ、なんて思ったりして(^^)。

それからは、小田原に行くときは、まずウメ子、そしてお城に登って、近くにある報徳神社内のcafe Gingerで一息ついて干物を買って帰るというルートが気に入ってたのに・・・。もうウメ子がいないなんて。

ウメ子は昭和25年、推定3歳のときに日本に来て以来59年。現在推定62歳で人間の年齢では約100歳。私なんかよりも長く日本を眺めてきた、ずっとずっと先輩だ。前日まで食べ物も残さず食べるなど元気だったらしいが急に亡くなったらしい。 象ほど大きくて強そうな生き物でも、不意に亡くなってしまうこともある。いつまでも小田原城にはウメ子がいるような気がしていたが、やっぱり変わらないものなんてない。

ウメ子が亡くなると、小田原城下の動物園そのものも閉園になるかもしれないとのこと。『史跡小田原城跡本丸・二の丸整備基本構想』という昔の小田原城を復元しようという構想のなかでは、「史跡にふさわしくない施設」として動物園は整理していくという方針が打ち出されているらしい。
小田原動物園の整理進む一タウンニュース

ウメ子に癒された一人として、小田原動物園という形は変わっても、あの場所が変わらず小田原の人たちの癒しや憩いの場であり続けてほしいと心から願っている。

2009年9月17日木曜日

GAE/Jでのインデックスの削除方法が分からない

検索するクエリを多く書いていくと、インデックスが増えていく。

GAE/Jでエンティティを抽出するときは、直接BigTable上のエンティティにフィルタリングしにいくのではなく、別途作成されたインデックスに対してフィルタリングを行う、という仕様になっているらしい。
参考:Java データストアのインデックスの設定 - Google App Engine - Google Code

インデックスが増加していくことは仕方ないことなのか、パフォーマンスなどのことを考えて、なるべくインデックスは増やさない方がいいのか、調査する。

すると、やはりGAE/Jでは==などの条件指定に限り、そのほかの条件による絞込みやソートはJavaのロジック側で実装した方が無難らしい。
参考:ローカルでは動作するが、GAE/J環境でエラーになってしまう - Slim3 User Japan | Google グループ

エンティティに変更が加えられるたびにインデックスが更新されるというシステムは、今後エンティティ量が増えていったとき、クエリに対してすばやく結果を返せるメリットもあるが、インデックスを更新するという作業の頻度や量が増えていくというデメリットも考えられる。以前、ほしい結果を全てクエリだけで実現できないかと思っていたが、それは少々的外れだったみたい。クエリはシンプルに徹底した方がよさそう。よって、コードを整理し、必要なインデックスを最低限に絞ることにした。

と、インデックスを削除しようとしたら、「あれ、どうすればいいんだ?」とスタックする。削除するインターフェースは見当たらないし、調べてみても削除方法について述べられているドキュメントがぱっと見当たらない。

とりあえず、war/WEB-INF/datastore-indexes.xml及びwar/WEB-INF/appengine-generated/datastore-indexes-auto.xmlを一旦削除。

ローカルでサーバーを起動し、ユニットテストするなりしてクエリを発行させて、自動的にdatastore-indexes-auto.xmlを作成。その記述内容を元にdatastore-indexes.xmlを作成しGAE/Jサーバー側へデプロイする。→インデックス、変化なし。

念のため、autoGenerate="false"にし、強制的にdatastore-indexes.xmlを使うようにしても変化無し。以下はそのときのdatastore-indexes.xml。



    
        
        
    


datastore-indexes.xmlの中身を空にしてデプロイしてみたが、それでも変化なし。


appengineのbinに含まれているappcfgコマンドを使ってみる。「0% Uploading index definitions.」ってことはインデックスの更新は一切行われなかったということか。よって削除されず、これでも変化なし。
C:\eclipse\plugins\com.google.appengine.eclipse.sdkbundle_1.2.5.v200909021031\ap
pengine-java-sdk-1.2.5\bin>appcfg.cmd update_indexes "C:\Documents and Settings\
xxx\workspace\myapp\war"
Reading application configuration data...
Beginning server interaction for myapp...
0% Uploading index definitions.
Success.
Cleaning up temporary files...
現在のインデックスに存在しない指定が含まれていた場合はインデックスが新規に作成されるが、一度作成されたインデックスを含まないような記述をdatastore-indexes.xmlに加えても、削除はされないのか?!・・・もしれないorz

「今出来るベストのことは、それらのインデックスを無視することだ」といっている会話もあったが、う~ん、結局無視するしかないのか?!
参考:Delete useless Entity and Indexes - Google App Engine for Java | Google グループ

キモチワルイ・・・。しかも実際には使用していないインデックスだが、エンティティが増えるたびにその未使用のインデックスの更新も同時に行われるということで、ムダだ。

また、ちなみに自分の環境だと、datastore-indexes-auto.xmlがmyapp/war/WEB-INF/appengine-generated/ではなく、warの外、つまりmyapp/WEB-INF/appengine-generated/の下に常に作成されるのが不便。いちいちコピペしなくてはならない。設定の問題?これもキモチワルイ・・・。

2009年9月16日水曜日

VistaでGAE/Jサーバーにlocalhostとしてアクセスするには

Windows VistaでローカルのGAE/Jサーバーにアクセスするときに、どうしてもGAE/Jのドキュメント通りに「http://localhost:8080」で接続できず、「http://127.0.0.1:8080」でないと接続できないでいた。

恐らくVistaがIPv6対応であることが影響しているんだろうとは思っていたものの放置していたが、

「C:\Windows\System32\drivers\etc\hosts」に

127.0.0.1 localhost

を追加すると、localhostで接続できることが分かった。ローカルのGAE/JサーバーはIPv4でしか待ち受けていないらしい。
参考:「Google App Engine for Java[実践]クラウドシステム構築」 P73

ログインしているユーザーでhostsが編集できるように、hostsファイルのセキュリティ設定を変更する必要があることにも注意。

2009年9月14日月曜日

不等式フィルタを使い且つ複数のプロパティでソート

緯度と経度のgeoboxで絞込みを行うことと、日付の降順でソートを行うこと、これらをクエリーだけで実装しようとトライした。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("String geohashFrom, String geohashTo");
query.setFilter("geohash >= geohashFrom && geohash <= geohashTo");
List mutters = (List) query.execute(geohashFrom,geohashTo);
query.setOrdering("geohash ASC, date DESC");
これだと、Geohashの昇順で並べられた中での日付降順という意味になってしまう。

ほしいのは、日付降順で並べられた、限られたgeoboxのデータリスト。つまり、ソートの順番で先に日付を持ってきたいのだが、Geohashの不等式を使っている以上、どうしてもソートの優先順位一位としてGeohashを指定しなければならない。これはGAE/J上の制約。
参考:クエリとインデックス - Google App Engine - Google Code

勿論、
query.setOrdering("date DESC, geohash ASC");
と書くと、例外が発生する。

NestedThrowablesStackTrace:
java.lang.IllegalArgumentException: The first sort property must be the same as the property to which the inequality filter is applied. In your query the first sort property is date but the inequality filter is on geohash

Query query = pm.newQuery(Mutter.class, "ORDER BY date DESC");
とも書いてみたが、例外は発生しないものの、日付降順ではソートされておらず。。。

結局、クエリでソートすることはやめて、JavaでComparatorを使いソートすることにした(^^;)。
import java.util.Comparator;

public class DateComparator implements Comparator<Mutter> {

 // デフォルトは降順
 private boolean order = false;

 public void setOrder(boolean order) {
  this.order = order;
 }

 public int compare(Mutter m0, Mutter m1) {
  if (this.order) {
   // 昇順
   return m0.getDate().compareTo(m1.getDate());
  } else {
   // 降順
   return m1.getDate().compareTo(m0.getDate());
  }
 }
}
取得したListをComparatorでソートする。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("String geohashFrom, String geohashTo");
query.setFilter("geohash >= geohashFrom && geohash <= geohashTo");
List<Mutter> mutters = (List<Mutter>) query.execute(geohashFrom,geohashTo);
Collections.sort(mutters, comparator);
これで日付降順&緯度経度の絞込みができるようになった。久しぶりにComparator書いたから、ちょっと書き方忘れてたよー(^^;)。うーん、でもやっぱりクエリだけでなんとかしたいと思うんだけど、無理かなぁ?

DatastoreNeedIndexException

実行時に例外が表示されるようになった。
com.google.appengine.api.datastore.DatastoreNeedIndexException : no matching index found.

表示されるようになったのは、ソートを2列で行う記述を追加してから。
query.setOrdering("geohash ASC, date DESC");

どうやらインデックスの設定が必要らしい。

GAE/Jのサイトを見ながらdatastore-indexes.xmlを記述。
参考:Java データストアのインデックスの設定 - Google App Engine - Google Code



    
        
        
    


datastore-indexes.xmlはwar/WEB-INF下に配置。
ローカルでGAE/Jサーバーを起動すると、war/WEB-INF/appengine-generated下にdatastore-indexes-auto.xmlが自動的に作成される。
この状態になってから、サーバー側へデプロイする。
そうでないとインデックスが作成されず、DatastoreNeedIndexExceptionが解消されない。

しかし、このデプロイ時に
"Deploying xxx to Google" 中に内部エラーが発生しました。
XML error validating xxx\war\WEB-INF\datastore-indexes.xml against C:\eclipse\plugins\com.google.appengine.eclipse.sdkbundle_1.2.5.v200909021031\appengine-java-sdk-1.2.5\docs\datastore-indexes.xsd
というエラーが解決できずにかなり嵌った。

結局、上記のGAE/Jのサイトにはxmlnsが含まれていたのだが、その記述が原因らしい。xmlnsを削除したら解消された。
参考:Issue 1545 - googleappengine - xml error while validating datastore-indexes.xml while deploying to appengine with sdk 1.2.1 - Project Hosting on Google Code

なんだよおー。頼みますよー。

datastore-indexes.xmlもdatastore-indexes-auto.xmlもあって、両ファイルの記述にも違いがないのにまだDatastoreNeedIndexExceptionが出る場合は、GAE/Jの管理パネルの「Datastore」→「Indexes」で作成したインデックスのステータスが「Building」ではなく「Serving」になっているか確認してください。

ローカルでGAE/Jサーバーが動かなくなった?!

ローカルでGAE/Jサーバーを立ち上げたら、例外が・・・
2009-09-14 03:55:04.869::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
2009-09-14 03:55:04.957::INFO:  jetty-6.1.x
2009-09-14 03:55:04.159::WARN:  failed _ah_StaticFileFilter
java.lang.ClassCastException: com.google.apphosting.utils.jetty.AppEngineWebAppContext$AppEngineServletContext cannot be cast to org.mortbay.jetty.handler.ContextHandler$SContext
 at com.google.appengine.tools.development.StaticFileFilter.init(StaticFileFilter.java:47)
 at org.mortbay.jetty.servlet.FilterHolder.doStart(FilterHolder.java:99)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:589)
 at org.mortbay.jetty.servlet.Context.startContext(Context.java:139)
 at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1218)
 at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:500)
 at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:448)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.jetty.Server.doStart(Server.java:217)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at com.google.appengine.tools.development.JettyContainerService.startContainer(JettyContainerService.java:152)
 at com.google.appengine.tools.development.AbstractContainerService.startup(AbstractContainerService.java:116)
 at com.google.appengine.tools.development.DevAppServerImpl.start(DevAppServerImpl.java:218)
 at com.google.appengine.tools.development.DevAppServerMain$StartAction.apply(DevAppServerMain.java:162)
 at com.google.appengine.tools.util.Parser$ParseResult.applyArgs(Parser.java:48)
 at com.google.appengine.tools.development.DevAppServerMain.<init>(DevAppServerMain.java:113)
 at com.google.appengine.tools.development.DevAppServerMain.main(DevAppServerMain.java:89)
2009-09-14 03:55:04.160::WARN:  Failed startup of context com.google.apphosting.utils.jetty.DevAppEngineWebAppContext@1d8d237{/,xxx\war}
java.lang.ClassCastException: com.google.apphosting.utils.jetty.AppEngineWebAppContext$AppEngineServletContext cannot be cast to org.mortbay.jetty.handler.ContextHandler$SContext
 at com.google.appengine.tools.development.StaticFileFilter.init(StaticFileFilter.java:47)
 at org.mortbay.jetty.servlet.FilterHolder.doStart(FilterHolder.java:99)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:589)
 at org.mortbay.jetty.servlet.Context.startContext(Context.java:139)
 at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1218)
 at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:500)
 at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:448)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at org.mortbay.jetty.handler.HandlerWrapper.doStart(HandlerWrapper.java:117)
 at org.mortbay.jetty.Server.doStart(Server.java:217)
 at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
 at com.google.appengine.tools.development.JettyContainerService.startContainer(JettyContainerService.java:152)
 at com.google.appengine.tools.development.AbstractContainerService.startup(AbstractContainerService.java:116)
 at com.google.appengine.tools.development.DevAppServerImpl.start(DevAppServerImpl.java:218)
 at com.google.appengine.tools.development.DevAppServerMain$StartAction.apply(DevAppServerMain.java:162)
 at com.google.appengine.tools.util.Parser$ParseResult.applyArgs(Parser.java:48)
 at com.google.appengine.tools.development.DevAppServerMain.<init>(DevAppServerMain.java:113)
 at com.google.appengine.tools.development.DevAppServerMain.main(DevAppServerMain.java:89)
2009-09-14 03:55:04.178::INFO:  Started SelectChannelConnector@127.0.0.1:8080
The server is running at http://localhost:8080/
あれ?と思いサーチしたら、なんとwar/WEB-INF/libの下にappengine-local-runtime.jarを配置しているとjettyの起動に失敗するらしい。
参考:I broke something using Eclipse plugin - Google App Engine for Java | Google グループ

ユニットテストがやりたくて、必要なjarをプロジェクト下に配置した。適当にWEB-INF/libの下に配置したが、それが問題だったのだorz。

よって、appengine-local-runtime.jarとappengine-api-stubs.jarをWEB-INF/lib下から削除し、別途WEB-INF/にjunitlibsというフォルダを作成。そのフォルダ下にappengine-local-runtime.jarとappengine-api-stubs.jarを配置した。また、ビルドパスもその場所へ設定を変更した。

そうすると
The following classpath entry 'xxx\war\WEB-INF\junitlibs\appengine-api-stubs.jar' will not be available on the server's classpath
The following classpath entry 'xxx\war\WEB-INF\junitlibs\appengine-local-runtime.jar' will not be available on the server's classpath
Eclipseの警告が2行でますが、ムシ(^^)。

appengine-api-stubs.jarは今回の問題に関係はないのですが、ユニットテストのために追加したjar2つをまとめておきたいので、appengine-local-runtime.jarと同じ場所に管理しました。

GAE/J-Blaze接続でInvocationTargetExceptionがよく起こる

クライアントのAirアプリからGAE/JのサーバーにアクセスするとよくInvocationTargetExceptionが起こる。

発生するタイミングとしては、アイドルタイムが10分以上経ったりなど、しばらくしてからサーバーへリクエストを発生した時によく起こる。というか必ず起こっている。

ログのレベルとしてはInfoレベルなのだが、Air上でも一瞬「うっ」とリクエストが止まったように見えて、なんだか気になる。
xxx.xxx.xxx.xxx - - [13/Sep/2009:18:47:23 -0700] "POST /messagebroker/amf;jsessionid=Z6NYw5g89TFfOj6cDi50aA HTTP/1.1" 200 960 "app:/xxx.swf" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/526.9+ (KHTML, like Gecko) AdobeAIR/1.5,gzip(gfe)" "xxx.appspot.com"

I 09-13 06:47PM 22.601

com.google.appengine.repackaged.com.google.common.base.FinalizableReferenceQueue$SystemLoader loadFinalizer: Not allowed to access system class loader.

I 09-13 06:47PM 22.614

com.google.appengine.repackaged.com.google.common.base.internal.Finalizer getInheritableThreadLocalsField: Couldn't access Thread.inheritableThreadLocals. Reference finalizer threads will inherit thread local values.

I 09-13 06:47PM 22.616

com.google.appengine.repackaged.com.google.common.base.FinalizableReferenceQueue <init>: Failed to start reference finalizer thread. Reference cleanup will only occur when new references are created.
java.lang.reflect.InvocationTargetException
 at com.google.appengine.runtime.Request.process-1ed2a922d6cd3b28(Request.java)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Method.java:40)
 at com.google.appengine.repackaged.com.google.common.base.FinalizableReferenceQueue.<init>(FinalizableReferenceQueue.java:124)
 at com.google.appengine.repackaged.com.google.common.labs.misc.InterningPools$WeakInterningPool.<clinit>(InterningPools.java:104)
 at com.google.appengine.repackaged.com.google.common.labs.misc.InterningPools.newWeakInterningPool(InterningPools.java:48)
 at com.google.appengine.repackaged.com.google.io.protocol.ProtocolSupport.<clinit>(ProtocolSupport.java:55)
 at com.google.apphosting.api.DatastorePb$Query.<init>(DatastorePb.java:1072)
 at com.google.apphosting.api.DatastorePb$Query$1.<init>(DatastorePb.java:2355)
 at com.google.apphosting.api.DatastorePb$Query.<clinit>(DatastorePb.java:2355)
 at com.google.appengine.api.datastore.QueryTranslator.convertToPb(QueryTranslator.java:27)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl.convertToPb(DatastoreServiceImpl.java:357)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl.runQuery(DatastoreServiceImpl.java:339)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl.access$100(DatastoreServiceImpl.java:269)
 at com.google.appengine.api.datastore.DatastoreServiceImpl$PreparedQueryImpl$1.iterator(DatastoreServiceImpl.java:303)
 at org.datanucleus.store.appengine.query.RuntimeExceptionWrappingIterable.iterator(RuntimeExceptionWrappingIterable.java:42)
 at org.datanucleus.store.appengine.query.StreamingQueryResult.<init>(StreamingQueryResult.java:77)
 at org.datanucleus.store.appengine.query.DatastoreQuery.newStreamingQueryResultForEntities(DatastoreQuery.java:324)
 at org.datanucleus.store.appengine.query.DatastoreQuery.fulfillEntityQuery(DatastoreQuery.java:310)
 at org.datanucleus.store.appengine.query.DatastoreQuery.performExecute(DatastoreQuery.java:242)
 at org.datanucleus.store.appengine.query.JDOQLQuery.performExecute(JDOQLQuery.java:84)
 at org.datanucleus.store.query.Query.executeQuery(Query.java:1489)
 at org.datanucleus.store.query.Query.executeWithArray(Query.java:1371)
 at org.datanucleus.jdo.JDOQuery.execute(JDOQuery.java:266)
 at jp.co.tricell.sonicboom.rpc.MutterUtils.getMutterAround(MutterUtils.java:84)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Method.java:40)
 at flex.messaging.services.remoting.adapters.JavaAdapter.invoke(JavaAdapter.java:421)
 at flex.messaging.services.RemotingService.serviceMessage(RemotingService.java:183)
 at flex.messaging.MessageBroker.routeMessageToService(MessageBroker.java:1503)
 at flex.messaging.endpoints.AbstractEndpoint.serviceMessage(AbstractEndpoint.java:884)
 at flex.messaging.endpoints.amf.MessageBrokerFilter.invoke(MessageBrokerFilter.java:121)
 at flex.messaging.endpoints.amf.LegacyFilter.invoke(LegacyFilter.java:158)
 at flex.messaging.endpoints.amf.SessionFilter.invoke(SessionFilter.java:44)
 at flex.messaging.endpoints.amf.BatchProcessFilter.invoke(BatchProcessFilter.java:67)
 at flex.messaging.endpoints.amf.SerializationFilter.invoke(SerializationFilter.java:146)
 at flex.messaging.endpoints.BaseHTTPEndpoint.service(BaseHTTPEndpoint.java:278)
 at flex.messaging.MessageBrokerServlet.service(MessageBrokerServlet.java:322)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
 at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
 at com.google.apphosting.runtime.jetty.SaveSessionFilter.doFilter(SaveSessionFilter.java:35)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
 at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
 at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
 at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.handle(AppVersionHandlerMap.java:237)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at org.mortbay.jetty.Server.handle(Server.java:313)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
 at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:830)
 at com.google.apphosting.runtime.jetty.RpcRequestParser.parseAvailable(RpcRequestParser.java:76)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
 at com.google.apphosting.runtime.jetty.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:139)
 at com.google.apphosting.runtime.JavaRuntime.handleRequest(JavaRuntime.java:235)
 at com.google.apphosting.base.RuntimePb$EvaluationRuntime$6.handleBlockingRequest(RuntimePb.java:4950)
 at com.google.apphosting.base.RuntimePb$EvaluationRuntime$6.handleBlockingRequest(RuntimePb.java:4948)
 at com.google.net.rpc.impl.BlockingApplicationHandler.handleRequest(BlockingApplicationHandler.java:24)
 at com.google.net.rpc.impl.RpcUtil.runRpcInApplication(RpcUtil.java:359)
 at com.google.net.rpc.impl.Server$2.run(Server.java:823)
 at com.google.tracing.LocalTraceSpanRunnable.run(LocalTraceSpanRunnable.java:56)
 at com.google.tracing.LocalTraceSpanBuilder.internalContinueSpan(LocalTraceSpanBuilder.java:516)
 at com.google.net.rpc.impl.Server.startRpc(Server.java:778)
 at com.google.net.rpc.impl.Server.processRequest(Server.java:351)
 at com.google.net.rpc.impl.ServerConnection.messageReceived(ServerConnection.java:437)
 at com.google.net.rpc.impl.RpcConnection.parseMessages(RpcConnection.java:319)
 at com.google.net.rpc.impl.RpcConnection.dataReceived(RpcConnection.java:290)
 at com.google.net.async.Connection.handleReadEvent(Connection.java:428)
 at com.google.net.async.EventDispatcher.processNetworkEvents(EventDispatcher.java:762)
 at com.google.net.async.EventDispatcher.internalLoop(EventDispatcher.java:207)
 at com.google.net.async.EventDispatcher.loop(EventDispatcher.java:101)
 at com.google.net.rpc.RpcService.runUntilServerShutdown(RpcService.java:251)
 at com.google.apphosting.runtime.JavaRuntime$RpcRunnable.run(JavaRuntime.java:392)
 at java.lang.Thread.run(Unknown Source)
Caused by: java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThreadGroup)
 at java.security.AccessControlContext.checkPermission(Unknown Source)
 at java.security.AccessController.checkPermission(Unknown Source)
 at java.lang.SecurityManager.checkPermission(Unknown Source)
 at java.lang.ThreadGroup.checkAccess(Unknown Source)
 at java.lang.Thread.init(Unknown Source)
 at java.lang.Thread.<init>(Unknown Source)
 at com.google.appengine.repackaged.com.google.common.base.internal.Finalizer.<init>(Finalizer.java:96)
 at com.google.appengine.repackaged.com.google.common.base.internal.Finalizer.startFinalizer(Finalizer.java:82)
 ... 81 more
上記のログには3種類のInfoレベルのログが含まれており、最後の3つ目のInfoレベルのものがInvocationTargetExceptionなのだが、この3種類のものが必ずアイドルタイム発生後に発生する。

InvocationTargetExceptionは、JavaDocによると「呼び出されるメソッドまたはコンストラクタがスローする例外をラップする、チェック済み例外」ということ。問題なのはログの下の方にある、原因となった「Caused by~」のほうだが、securityのAccessControlExceptionということで、これはGAE/J上でスレッドなどを走らせようとしたときによく出る例外だったなぁと思いだす。

Airから接続して前回のセッションを呼び出そうとした時にスレッドを使おうとしてExceptionが発生しているのかもしれない。もともとBlazeDSのRemoting Objectもコードを改変してムリクリ動かしているわけで(^^;)。

動作上は、ちょっとリクエストがつっかえたように見えるというくらいで、その他は期待したとおりの動作が出来ているので問題はないのだが・・・。

毎回このログが出ないようにできれば改善したい。どうにかできんもんかのー。

2009年9月12日土曜日

Only one inequality filter per query is supported

GAE/J上では複数のプロパティーに対して不等式フィルタが使えないということが分かった(┳◇┳)
参考:サンフランシスコの 24 時間: Geolocation App - Google App Engine - Google Code
App Engine のクエリには固有の制約があり、複数のプロパティ(経度と緯度など)に対して不等式フィルタを実行できないからです。
ノーorz。
まさにこの、”緯度経度で絞り込む”ではまっていたが、結局ずっと例外が出て・・・。
NestedThrowablesStackTrace:
java.lang.IllegalArgumentException: Only one inequality filter per query is supported.  Encountered both latitude and longitude
当初、コードは以下のように書いていた。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("double latitudeFrom,  double latitudeTo, double longitudeFrom,double longitudeTo");
query.setFilter("latitude >= latitudeFrom && latitude <= latitudeTo && longitude >= longitudeFrom && longitude <= longitudeTo");
query.setOrdering("date Desc");
List<Mutter> mutters = (List<Mutter>) query.execute(latitudeFrom, latitudeTo, longitudeFrom, longitudeTo);
pm.close();
でもこれだと、コンパイルエラーになる。理由はQueryのexecuteメソッドは引数3つまでのメソッドしかないから。

よって、以下のようにコードを変えてみた。
Query query = pm.newQuery(Mutter.class);
query.declareParameters("double latitudeFrom,  double latitudeTo, double longitudeFrom,double longitudeTo");
query.setFilter("latitude >= latitudeFrom && latitude <= latitudeTo && longitude >= longitudeFrom && longitude <= longitudeTo");
query.setOrdering("date Desc");
Object[] paramArray = { latitudeFrom, latitudeTo, longitudeFrom,longitudeTo };
List<Mutter> mutters = (List<Mutter>) query.executeWithArray(paramArray);
pm.close();
すると、上記のように「Only one inequality filter per query is supported.」が出て嵌る。 executeWithArrayメソッドの使い方がまずいのかと思い(結局これは見当違いだったが)、じかにクエリーを書く形式に変えてみる。
// SQL作成
String query = "SELECT FROM " + Mutter.class.getName()
  + " WHERE latitude >= " + latitudeFrom + " && latitude <= "
  + latitudeTo + " && longitude >= " + longitudeFrom
  + " && longitude <= " + longitudeTo + " ORDER BY date DESC";

PersistenceManager pm = PMF.get().getPersistenceManager();
List<Mutter> mutters = (List<Mutter>) pm.newQuery(query).execute();
pm.close();
でも相変わらず「Only one inequality filter per query is supported.」

で、結局冒頭に書いたとおり、それはGAE/Jでの制約だったのだ。

上記サイトで触れてあった、地図の境界ボックスを計算するオープンソースライブラリを見てみるもののPythonで書かれていたので避け、もう一方の方法Geohashを使うやり方でやってみた。

Geohashは緯度と経度の位置データから1つの文字列を生成する方法。どのような文字列が生成されるのかは、以下のサイトでテキストボックスに適当な緯度と経度を入力して確認できる。例えば「35.438399 139.361765」を入力すると神奈川県厚木市のイトーヨーカドーのGeohashが求められる。
参考:Geohash - geohash.org

GAE/Jのサイトでは、「ただし、この手法には、特に世界の特定地域において限界があります。」と書いてあったのが気になるが、とりあえず採用。Geohashのエンコードとデコードが書かれたJavaのライブラリが既にあったので、それを使ってみることにする。この辺はこちらを参考に。
参考:琴線探査: Geohashだなぁ
public boolean addMutter(String userName, double latitude,
  double longitude, String message, int boomColor, int radius) {
 boolean ok = false;

 // 緯度経度をGeohashでエンコード
 String geohash = Geohash.encode(latitude, longitude);

 Mutter mutter = new Mutter(userName, latitude, longitude, geohash,
   new Date().getTime(), message, boomColor, radius);

 PersistenceManager pm = PMF.get().getPersistenceManager();

 try {
  pm.makePersistent(mutter);
  ok = true;
 } finally {
  pm.close();
 }

 return ok;
}

public List getMutterAround(double latitudeFrom,
  double longitudeFrom, double latitudeTo, double longitudeTo) {

 // geoboxの緯度経度(南西から北東)をGeohashにエンコード
 String geohashFrom = Geohash.encode(latitudeFrom, longitudeFrom);
 String geohashTo = Geohash.encode(latitudeTo, longitudeTo);

 PersistenceManager pm = PMF.get().getPersistenceManager();

 Query query = pm.newQuery(Mutter.class);
 query.declareParameters("String geohashFrom, String geohashTo");
 query.setFilter("geohash >= geohashFrom && geohash <= geohashTo");
 query.setOrdering("geohash ASC");
 List<Mutter> mutters = (List<Mutter>) query.execute(geohashFrom,
   geohashTo);
 log.info("緯度" + latitudeFrom + "から" + latitudeTo + ", 経度"
   + longitudeFrom + "から" + longitudeTo + "までのデータは"
   + mutters.size() + "件です");

 pm.close();

 return mutters;
}
結果は成功。Geohashを使って、不等式を使わずに緯度経度での絞り込みが出来ました♪

Object Manager has been closed

GAE/Jベースで、検索するメソッドのテストケースを書いていたら実行時に以下の例外が出た。
Object Manager has been closed
org.datanucleus.exceptions.NucleusUserException: Object Manager has been closed
 at org.datanucleus.ObjectManagerImpl.assertIsOpen(ObjectManagerImpl.java:3876)
 at org.datanucleus.ObjectManagerImpl.getFetchPlan(ObjectManagerImpl.java:376)
 at org.datanucleus.store.query.Query.getFetchPlan(Query.java:497)
 at org.datanucleus.store.appengine.query.DatastoreQuery$5.apply(DatastoreQuery.java:508)
 at org.datanucleus.store.appengine.query.DatastoreQuery$5.apply(DatastoreQuery.java:507)
 at org.datanucleus.store.appengine.query.StreamingQueryResult.resolveNext(StreamingQueryResult.java:137)
 at org.datanucleus.store.appengine.query.StreamingQueryResult$1.computeNext(StreamingQueryResult.java:163)
 at org.datanucleus.store.appengine.query.AbstractIterator.tryToComputeNext(AbstractIterator.java:132)
 at org.datanucleus.store.appengine.query.AbstractIterator.hasNext(AbstractIterator.java:127)
 at org.datanucleus.store.appengine.query.StreamingQueryResult$AbstractListIterator.hasNext(StreamingQueryResult.java:229)
 at jp.co.tricell.sonicboom.rpc.test.TestMutterUtils.testGetMutterOf(TestMutterUtils.java:44)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at junit.framework.TestCase.runTest(TestCase.java:164)
 at junit.framework.TestCase.runBare(TestCase.java:130)
 at junit.framework.TestResult$1.protect(TestResult.java:106)
 at junit.framework.TestResult.runProtected(TestResult.java:124)
 at junit.framework.TestResult.run(TestResult.java:109)
 at junit.framework.TestCase.run(TestCase.java:120)
 at junit.framework.TestSuite.runTest(TestSuite.java:230)
 at junit.framework.TestSuite.run(TestSuite.java:225)
 at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestReference.run(JUnit3TestReference.java:130)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
むぅーと思っていたら、ひがさんのブログでちょうど参考になる記事発見。
参考:JDOのモデルの状態を理解しよう - ひがやすを blog

テスト元メソッドは以下。つまりPersistenceManagerをclose()したのに、テストケースの方でクエリの結果を触ろうとしていたので「Object Manager has been closed」が出たということ。
// クラスのアノテーションを記述
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Mutter {
・・・・・
}

public class MutterUtils {
・・・・・

 public List<Mutter> getMutterOf(String myKey) {
  PersistenceManager pm = PMF.get().getPersistenceManager();

  Query query = pm.newQuery(Mutter.class);
  query.declareParameters("String myKey");
  query.setFilter("userName == myKey");
  query.setOrdering("date DESC");
  List<Mutter> mutters = (List<Mutter>) query.execute(myKey);

  pm.close();

  return mutters;
 }
}
ひがさんの記事を参考に、クラスのアノテーションに太字部分を追加。
@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable="true")。

これでPersistenceManagerをclose()した後でも触れるdetachedなモデルになった。

さらに、PersistenceManager#detachCopy()やPersistenceManager#detachCopyAll()を呼ぶのが面倒なので、jdoconfig.xmlに以下のpropertyタグを追加。
<property name="datanucleus.DetachOnClose" value="true"/>

あわせて、JDOのモデルには以下の4パターン
  • transient
  • persistent
  • detached
  • hollow
があるということも学んだゾ。

Google App EngineでUnit Test

GAE/JでUnit Testするには、いくつか設定が必要。設定方法は、ちゃんとGAE/Jのページにまとめてあります。素晴らしい。
参考:Unit Testing With Local Service Implementations - Google App Engine - Google Code

1.テスト実行環境を作る

まずは、appengine-api-stubs.jarappengine-local-runtime.jarへクラスパスを通す。これらはappengineのsdkをダウンロードしたフォルダのlib/implの下にあります。 分かり易いように、GAE/Jのプロジェクトのwar/WEB-INF/lib下にこれらのjarをコピー。プロジェクトのプロパティーから「Javaのビルドパス」を選び「JARの追加」でクラスパスを通します。
2009.09.14追記:appengine-local-runtime.jarをWEB-INF/lib下に配置しておくと、ローカルでのGAE/Jサーバーの起動に失敗します。jarはlib以外に配置してください。詳しくは「ローカルでGAEサーバーが動かなくなった?!

その後、上記ページを参考にして(というか、そっくりコピペして)、TestEnvironmentクラスを作成。
import com.google.apphosting.api.ApiProxy;

import java.util.HashMap;
import java.util.Map;

class TestEnvironment implements ApiProxy.Environment {
 public String getAppId() {
  return "test";
 }

 public String getVersionId() {
  return "1.0";
 }

 public String getEmail() {
  throw new UnsupportedOperationException();
 }

 public boolean isLoggedIn() {
  throw new UnsupportedOperationException();
 }

 public boolean isAdmin() {
  throw new UnsupportedOperationException();
 }

 public String getAuthDomain() {
  throw new UnsupportedOperationException();
 }

 public String getRequestNamespace() {
  return "";
 }

 public Map<String, Object> getAttributes() {
  return new HashMap<String, Object>();
 }

}

2.基本クラスを作成

1で作成したTestEnvironmentを継承してLocalServiceTestCaseクラスを作る。ローカルサービスを簡単にテストできるようにするための基本クラス。
import java.io.File;

import com.google.appengine.tools.development.ApiProxyLocalImpl;
import com.google.apphosting.api.ApiProxy;

import junit.framework.TestCase;

public class LocalServiceTestCase extends TestCase {

 @Override
 public void setUp() throws Exception {
  super.setUp();
  ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment());
  ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")) {
  });
 }

 @Override
 public void tearDown() throws Exception {
  // not strictly necessary to null these out but there's no harm either
  ApiProxy.setDelegate(null);
  ApiProxy.setEnvironmentForCurrentThread(null);
  super.tearDown();
 }

}

3.Datastore Testのための拡張クラス作成

今回、テスト目的がデータストアのチェックだったので、データストアの中身をチェックする時、毎回データをクリーンにしてからテストが実行できるように、LocalServiceTestCaseを継承した以下のようなLocalDatastoreTestCaseクラスも作成。
import com.google.appengine.api.datastore.dev.LocalDatastoreService;
import com.google.appengine.tools.development.ApiProxyLocalImpl;
import com.google.apphosting.api.ApiProxy;

public class LocalDatastoreTestCase extends LocalServiceTestCase {

 @Override
 public void setUp() throws Exception {
  super.setUp();
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
  proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY,
    Boolean.TRUE.toString());
 }

 @Override
 public void tearDown() throws Exception {
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
  LocalDatastoreService datastoreService = (LocalDatastoreService) proxy
    .getService("datastore_v3");
  datastoreService.clearProfiles();
  super.tearDown();
 }

}

4.TestCaseを書いてみよう

ここまで終わってようやく自分のテストケースを実行できるようになりました。早速テストケースを書いて実行してみます。3で作ったLocalDatastoreTestCaseを継承して作ります。
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Query;

import hoge.Mutter;
import hoge.rpc.MutterUtils;
import hoge.test.LocalDatastoreTestCase;

public class TestMutterUtils extends LocalDatastoreTestCase {

 public void testAdd() {
  MutterUtils mu = new MutterUtils();

  //追加処理

  //追加後データ件数が1件増えていることを確認
  Query query = new Query(Mutter.class.getSimpleName());
  assertEquals(1, DatastoreServiceFactory.getDatastoreService().prepare(
    query).countEntities());
 }

 public void testGetMutterOf() {
  MutterUtils mu = new MutterUtils();

  //追加処理

  //追加後データ件数が1件増えていることを確認
  List<Mutter> mutters = mu.getMutterOf(userName);
  Query query = new Query(Mutter.class.getSimpleName());
  assertEquals(1, DatastoreServiceFactory.getDatastoreService().prepare(
    query).countEntities());

  //追加後のデータを検証
  for (Mutter mutter : mutters) {
   assertEquals("期待した文字列", mutter.getMessage());
  }

 }

}
テストケースを右クリックして「実行」→「JUnitテスト」をクリックしてテストを実行します。

やっぱりローカルでテストできると安心ですねぇ。

2009年9月10日木曜日

BigTableについてのビデオとスライド

BigTableの内部構造について勉強になった。
ちょっと長いけど見るに値する。



スライドは以下から見れる。
Under the covers of the App Engine Datastore by Ryan Barrett

2009年9月9日水曜日

Google App EngineでStreamingAMFChannelが使えたかとぬかった

昨日の記事に書いた以下の例外メッセージを見ていて、要はBaseStreamingHTTPEndpoint.javaの833行目にあるコード「currentThread.setName(threadName);」、つまりスレッドにsetNameしている部分をコメントしてみたら解決するのではないかということに。
2009/09/08 4:57:25 com.google.apphosting.utils.jetty.JettyLogger warn
警告: /messagebroker/streamingamf
java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThread)
 at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)
 at java.security.AccessController.checkPermission(AccessController.java:546)
 at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:139)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkAccess(DevAppServerFactory.java:178)
 at java.lang.Thread.checkAccess(Thread.java:1263)
 at java.lang.Thread.setName(Thread.java:1050)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.handleFlexClientStreamingOpenRequest(BaseStreamingHTTPEndpoint.java:833)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.serviceStreamingRequest(BaseStreamingHTTPEndpoint.java:1022)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.service(BaseStreamingHTTPEndpoint.java:430)
 at flex.messaging.MessageBrokerServlet.service(MessageBrokerServlet.java:322)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
 at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
 at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:121)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
 at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
 at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
 at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:54)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:313)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at org.mortbay.jetty.Server.handle(Server.java:313)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
 at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:844)
 at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:644)
 at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
 at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
 at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
コメントしてビルドしなおし、再びStreaimingAMFChannelを使って接続を試してみると・・・

java.security.AccessControlExceptionが出ない(>w<)!
やった!GAE/Jのセキュリティサンドボックスにひっかからずにいけたみたい!

ローカルのGAE/Jのプロジェクトで、StreamingAMFChannelを使って各種ブラウザとチャット通信を試してみる。IE8、Firefox、Chrome、Safari、Opera、Sleipnir、Lunascape5など。

IE系エンジンを使ったブラウザでは、consumer.subscribe();しても反応が無く、再読み込みして再度subscribeすると
[BlazeDS]Endpoint with id 'my-streaming-amf' cannot grant streaming connection to FlexClient with id 'F1B77645-EE1C-F1B8-66AA-7714CC07D614' because max-streaming-connections-per-session limit of '1' has been reached.
とエラーが出て、Consumerの接続に失敗する。IEならではのセッション周りの問題かと思い、BlazeDSのドキュメントを参考に、services-config.xmlのStreamingAMFChannelの設定にpropertiesを追加する。
        
            
            
              
                
                
              
            
        
IE系エンジンでも無事にsubscribe出来るようになった。

う~ん、さすがStreaming。文字列表示がサックサク。pollingの場合は文字列が表示されるまで一呼吸ある感じがモッサリして見えるがStreamingはそんなことはない。やっぱりStreamingの方がNearリアルタイム通信だなぁとルンルンでGAE/J上にデプロイしてみると・・・

動かない・・・orz

ぬかった。ぬかった。完全にぬか喜びだった。ノー。ConsumerもProducerもchannelFaultが発生。GAE/Jの管理パネルからログを見てみると、
[<GAE/Jアプリケーション名>/1.336207119101686921].: [BlazeDS]Endpoint with id 'my-streaming-amf' cannot service the streaming request as either the supplied FlexClient id 'F2421357-2D0B-B9BA-A2B2-6F5794E9859B is not valid, or the FlexClient with that id is not valid.
というログが残っていた。このエラーメッセージを表示していたflex.messaging.endpoints.BaseStreamingHTTPEndpoint.javaの1005行目から1019行目までをコメントしてみたらどうかということに(^^;ソースいじり放題だな)
/*if (!command.equals(CLOSE_COMMAND) && !validFlexClientId)
        {
            if (Log.isError())
                log.error("Endpoint with id '" + getId() + "' cannot service the streaming request as either the supplied"
                        + " FlexClient id '" + flexClientId + " is not valid, or the FlexClient with that id is not valid.");

            try
            {
                // Return an HTTP status code 400 to indicate that the client's request was syntactically invalid (invalid id).
                res.sendError(HttpServletResponse.SC_BAD_REQUEST);
            }
            catch (IOException ignore)
            {}
            return; // Abort further server processing.
        }*/
またビルドしなおしてローカルで動作確認→OK!→GAE/Jにデプロイ→GAE/J上で動作確認→失敗orz

ログにエラーは出なくなったが、Consumerにsubscribeしてもウンともスンとも言わない。もちろんチャットのメッセージを書いて送っても何のリアクションもない。動かない。。。


つまりはこれがクラウドや分散環境ということかもしれない。

Streamingのように静的に特定のサーバーとコネクションをはり続けるようなアプリケーションは構造的にムリというか向かないということだろう。1つのクライアントからのセッション情報が重複してしまうという最初のトラブルからそもそもそういうことだったのだ。

セッションがGAE/Jでは使えないということではないが、特定のサーバーとずっと通信するのではなく、分散環境で、どのサーバーと通信されているのか分からないし知らなくてもいいようなクラウドの世界では、セッションメインのWebアプリケーションの作成手法は一旦頭から追い出さないといけない。自分でサーバーもメンテし運用していると、そういう従来のクセがついつい出てしまう。

やってみてわかることもあるものだ。

2009年9月8日火曜日

Google App EngineでBlazeDSのLong Polling機能を試す

結論から言います。
できませんでした。orz

アドレスへアクセスして動作を確認しようとすると、一瞬、接続が確立されたように見えるのですが、その後すぐにConsumerがchannelFaultイベントをはき、ProducerもchannelFaultイベントをはく。そしてまた一瞬ConsumerがchannelConnectするものの、すぐにchannelFaultイベントをはくという動作になりました。結局接続が安定的に確立できず・・・

前回のMessagingのテスト状況からの変更点は以下。

1.WEB-INF/flex/services-config.xmlのchannelsにLongPollのチャンネルを追加
        
          
          
            true
            0
            60000
            3000
            100
          
        
BlazeDSのドキュメントによると、polling-interval-secondsタグではなくpolling-interval-millisと書いてある部分もあるので、どちらも試してみました。動作は変わりませんでした。

2.WEB-INF/flex/messaging-config.xmlのdefault-channelsにLongPollのチャンネルを追加
    
        
        
    
3. mxmlのChannelSetを変更
      private function onClickButton():void{
       
       consumer.unsubscribe();
       
       if(local.selected){
         pollAmfChannel.url="http://127.0.0.1:8080/messagebroker/amfpolling";
         longPollAmfChannel.url="http://127.0.0.1:8080/messagebroker/myamflongpoll";
       }else{
         pollAmfChannel.url = "http://xxx.appspot.com/messagebroker/amfpolling";
         longPollAmfChannel.url = "http://xxx.appspot.com/messagebroker/myamflongpoll";
       }
       
       consumer.subscribe();
      }
    ]]>
  
  
  
    
  

  
    
  

 
 
ConsumerもProducerもchannelSetの値を{pollChannelSet}にすると、前回の状況と同じで、単純にpollingを使ったMessagingになります。それを、どちらも{longPollChannelSet}を指定したり、Consumerのみ{longPollChannelSet}にしたりしましたが、結局、安定的に接続が確立されませんでした。

Comet的な処理はGAE/Jではできないという記事もあり、Long Pollingは今のところGAE/Jでは動作しないのかもしれません。Pullではなく、Push的な動作をGAE/J上で実現するには、何か別の手段を考えなければならない。

Google App EngineでBlazeDSのMessagingを試す

GAE上でBlazeのROは使えることは確認したが、Messagingを使ってチャットのようなNearリアルタイム通信は可能なのか試してみる。Messagingといっても、StreamingAMFChannelを使ったものや、AMFChannelのpollingを使ったものがあるが、とりあえずはStreamingAMFChannelが使えるかチェックしてみる。

[環境]

Remotingを試した時と変わっていません。
  • OS : Windows Vista SP2
  • IDE : Eclipse v3.4.2(Ganymade)
  • SDK : appengine-sdk-1.2.5/JDK1.6.0_07/Flex Builder3.2.0
  • ライブラリ : BlazeDS3.2.0.3978

[サーバーサイド]

1.WEB-INF/flex/messaging-config.xmlにdestinationとしてid=chatを追加

GAEのプロジェクト配下にあるWEB-INF/flex/messaging-config.xmlにdestinationを追加。これがmxml側で指定するdestination先になる。


    
        
        
    

    
        
    
    
    



2.WEB-INF/flex/services-config.xmlのchannelsにStreamingAMFChannelが設定されていることを確認
    

        
            
        

    

[クライアントサイド]

1.mxmlを作成

ローカルとGAEへデプロイした時と接続するチャンネルのパスが変わるので、ラジオボタンで簡単に接続先を設定できるようにしています。BlazeDSのサンプルにあるtestdrive-chatをベースにしています。

  
  
    
  
  
    


 
 

  
    
    
    
  

  
    
    
       
       
    
  
  


2.swfなどのコンテンツをGAEのプロジェクトへ配置

swf、html、AC_OETags.js、playerProductInstall.swfをGAEプロジェクト下のwarに配置。history機能を使う場合はhistoryフォルダもwarに配置する。


[動作確認]

ローカル上で動作確認。Streaming通信に
失敗・・・ノーorz
2009/09/08 4:57:25 com.google.apphosting.utils.jetty.JettyLogger warn
警告: /messagebroker/streamingamf
java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThread)
 at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)
 at java.security.AccessController.checkPermission(AccessController.java:546)
 at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:139)
 at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkAccess(DevAppServerFactory.java:178)
 at java.lang.Thread.checkAccess(Thread.java:1263)
 at java.lang.Thread.setName(Thread.java:1050)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.handleFlexClientStreamingOpenRequest(BaseStreamingHTTPEndpoint.java:833)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.serviceStreamingRequest(BaseStreamingHTTPEndpoint.java:1022)
 at flex.messaging.endpoints.BaseStreamingHTTPEndpoint.service(BaseStreamingHTTPEndpoint.java:430)
 at flex.messaging.MessageBrokerServlet.service(MessageBrokerServlet.java:322)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
 at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:487)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1093)
 at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:121)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1084)
 at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:360)
 at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
 at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:181)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:712)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:405)
 at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:54)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:313)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:139)
 at org.mortbay.jetty.Server.handle(Server.java:313)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:506)
 at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:844)
 at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:644)
 at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:211)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:381)
 at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:396)
 at org.mortbay.thread.BoundedThreadPool$PoolThread.run(BoundedThreadPool.java:442)
GAEのセキュリティサンドボックスにひっかかってしまったようだ。よって、Martinの記事を参考に、StreamingAMFChannelではなく、AMFChannelのpollingを使って動作チェックしてみる。
参考:Martin's Blog: AppEngine & BlazeDS (Messaging)


[エラー解決までの道のり]

1.mxmlでStreamingAMFChannelを使っていた箇所をAMFChannelのpollingを使った記述へ変更。以下、変更部部分。
      private function onClickButton():void{
       
       consumer.unsubscribe();
       
       if(local.selected){
        amfchannel.url="http://127.0.0.1:8080/messagebroker/amfpolling";
       }else{
        amfchannel.url = "http://xxx.appspot.com/messagebroker/amfpolling";
       }
       
       consumer.subscribe();
      }
    ]]>
  
  
  
    
  

2.WEB-INF/flex/services-config.xmlのchannelsにAMFChannelが設定されていることを確認
2秒おきにポーリングするようにしてみました。
    

        
            
            
                true
                2
            
        

    


3.通信テスト
ローカル上で通信が確立することを確認!イェイ。
GAE上にデプロイして動作確認。
通信成功!やたっ!

結局、polling使わないとGAEではダメだった。今度はLong Pollingを試してみよう。

2009年9月7日月曜日

Google App EngineでBlazeDSのRemoting通信を試す

Google App Engine(以下GAE/J)でBlazeDSのRemotingObjectを拡張したリモーティング通信を試してみる。

[環境]
  • OS : Windows Vista SP2
  • IDE : Eclipse v3.4.2(Ganymade)
  • SDK : appengine-sdk-1.2.5/JDK1.6.0_07/Flex Builder3.2.0
  • ライブラリ : BlazeDS3.2.0.3978

[サーバーサイド]

1.Eclipse上でWeb Application Projectとして新規プロジェクトを作成

2.1で作ったプロジェクト下にBlazeDS用のライブラリや設定ファイルをコピー

デプロイ図
BlazeDSをダウンロードして展開するとblazeds.warがある。それが基本的にBlazeDSの動作に最低限必要なコンポーネントになっている。

なのでblazeds.warを展開し、WEB_INFの下にある、flexフォルダ(4つのxmlファイルがある)、及びlibフォルダ(12個のjarファイルがる)をコピーし、GAE/Jのプロジェクト/war/WEB-INF下に配置する。

プロジェクト内のリソースの配置は以下。赤枠は新規追加したファイル。青枠はプロジェクト作成時に自動生成されるファイルで変更が必要なファイル。









3.クラス作成

動作確認のため、HelloWorld的な簡単な文字列を返すクラスを作ってみる。コードはこんな感じ。
package jp.co.tricell.sonicboom.rpc;
import java.util.logging.Logger;
public class HelloDS {
 // ロガーを指定
 private static final Logger log =  Logger.getLogger(HelloDS.class.getName());
 public String sayHello(String name) {
  log.info("input str = " + name);
  return "こんにちは、" + name + "さん!";
 }
}

4.WEB-INF/flex/remoting-config.xmlにdestinationを追加

HelloDSクラスをリモーティング接続先として設定ファイルに追加する。SyntaxHighlighterが一行だけで閉じるxmlのタグの書き方に対応していないのでワザワザ</adapter-definition>と書いてますが、実際は<adapter-definition id=~ default="true"/>でOKです。


    
        
    

    
        
    
    
    
          
              jp.co.tricell.sonicboom.rpc.HelloDS
          
    



5.WEB-INF/flex/services-config.xmlのsystemタグにmanageableの値falseを追加

これは今回の環境で必要な設定なのか不明だが、GAE/J上でBlazeDSを動作させるために参考にしたいくつかのページで記述されていたので設定として追加。
参考:
・Martin's Blog: AppEngine & Adobe BlazeDS (fix)
・Flex/AIRハマり帳 ~第2回 Google App Engine for Java+BlazeDSでハマらない方法・後編~ | デベロッパーセンター
    
 
        
        false
 
        
            false
            
        
    

6.WEB-INF/appengine-web.xmlのsessions-enabledタグに値trueを追加

セッションを有効にするため設定を追加する。
 true

 
 
  
 

7.WEB-INF/web.xmlにMessageBroker Servletを追加

  
        flex.messaging.HttpFlexSession
  
  
  
    
        MessageBrokerServlet
        MessageBrokerServlet
        flex.messaging.MessageBrokerServlet
        
            services.configuration.file
            /WEB-INF/flex/services-config.xml
       
       1
    
    
        MessageBrokerServlet
        /messagebroker/*
    

[クライアントサイド]

Airアプリを作ってみました。encpoint先はお持ちのGAE/Jのドメインを指定してください。
タグを含んだコードの場合、SyntaxHighlighterがコードを全て、Firefoxなどは小文字(<mx:WindowedApplication~→<mx:windowedapplication~など)、Internet Explorerの場合は大文字に展開してしまいます。コードをコピーする場合は大文字小文字に注意してください。

  
  
    
  
   
      
      
      
   


[動作確認]

Airアプリをクライアントとしてつくり、HelloDSと通信させてみた。但し!!!ここで問題が・・・
GAE/J上にデプロイしたクラスと通信させてみたら、以下のようなメッセージが表示され実行失敗orz。
[RPC Fault faultString="Detected duplicate HTTP-based FlexSessions, generally due to the remote host disabling session cookies. Session cookies must be enabled to manage the client connection correctly." faultCode="Server.Processing.DuplicateSessionDetected" faultDetail="null"]
at mx.rpc::AbstractInvoker/http://www.adobe.com/2006/flex/mx/internal::faultHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AbstractInvoker.as:220]
at mx.rpc::Responder/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\Responder.as:53]
at mx.rpc::AsyncRequest/fault()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\rpc\AsyncRequest.as:103]
at NetConnectionMessageResponder/statusHandler()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\messaging\channels\NetConnectionChannel.as:569]
at mx.messaging::MessageResponder/status()[C:\autobuild\3.2.0\frameworks\projects\rpc\src\mx\messaging\MessageResponder.as:222]
GAE/Jが自動的に複数のサーバノード上でサーブレットを展開するために、1つのクライアントからのセッション情報が重複したように見えてしまうのが原因らしい。参考サイトを元にソースコードを編集してビルドする。
参考:http://prepro.wordpress.com/2009/05/17/googleappengineでblazeds環境を構築してみた


[エラー解決までの道のり]

1.BlazeDSのソースコードをダウンロード
BlazeDSのページからソースコードをダウンロードする。

2.BaseHTTPEndpoint.javaのコードの一部をコメント
ソースコードのzipを展開。編集が必要なソースは、flex.messaging.endpoints.BaseHTTPEndpoint.java。 ここの405行目からのduplicateSessionDetectedのチェックをコメントする。
/*if (duplicateSessionDetected)
        {
            List sessions = flexClient.getFlexSessions();
            for (Iterator iter = sessions.iterator(); iter.hasNext();)
            {
                FlexSession session = (FlexSession)iter.next();
                if (session instanceof HttpFlexSession)
                    session.invalidate();
            }
            
            // Return an error to the client.
            DuplicateSessionException e = new DuplicateSessionException();
            e.setMessage(ERR_MSG_DUPLICATE_SESSIONS_DETECTED);
            throw e;
        }*/
return flexClient;

3.ソースのビルドのため、Antをダウンロード
Apache Ant 1.7.1をダウンロード。

4.ソースのビルドのため、Ant-CONTRIBをダウンロード
Ant-CONTRIB 1.0b3をダウンロード。
展開してできるlibフォルダ内に有るant-contrib-1.0b3.jarをantフォルダのlib内にコピペ。

5.環境変数としてANT_HOMEを設定

6.環境変数PATHに%ANT_HOME%\binを追加

7.ビルド実行
BlazeDSのソースコードを展開したフォルダから
#ant clean
#ant make
を実行。
ビルド失敗orz
BUILD FAILED
C:\blazeds-src-3.2.0.3978\build.xml:73: The following error occurred while executing this line:
build.xml:156: Unable to rename old file
(C:\blazeds-src-3.2.0.3978\lib\flex-messaging-common.jar) to temporary file
編集したコードが含まれるflex-messaging-common.jarが権限が足りず変更できないらしい。はぅー。
参考:Adobe Forums: build blazeDS source code

結局Windows環境では権限をフルコントロールにしてもビルドエラーがどないもこないも解決できなかったので、Macでビルド。
ビルド成功!ほー(^-^;)

新しく出来た、flexから始まる5つのjarをGAE/JのプロジェクトのWEB-INF/libにコピペ。

8.通信テスト
GAE/J上にデプロイし、Airクライアントと通信。
通信成功ぉ~~!やたっ!

Windows環境でビルドに困る人のために、修正したコードでビルド済みのjarファイル5つを公開しま~す。この5つのjarをGAE/JのプロジェクトのWEB-INF/libにコピペしてGAE/Jへデプロイすればビルドせずにリモーティング通信できると思います。
元ソース:
blazeds-src-3.2.0.3978.zip

修正後jarファイル:
flex-messaging-common.jar
flex-messaging-core.jar
flex-messaging-opt.jar
flex-messaging-proxy.jar
flex-messaging-remoting.jar