こんにちは。このエントリはJava EE Advent Calendar 2013の17日目です。昨年に引き続き17日目を担当します。昨日は瑞鳳教徒・@btnrougeの「JSR 356―Java標準のWebSocket API」でした。明日は@kokuzawaさんです。
§1. 事の発端
私は19日のAdvent Calendar(JavaとJavaFX)に向けてJavaFX+JavaMailの試作品を作っていまして、全然暇じゃなかったのですが(後期ほとんど学校に行ってないですけど)、@btnrougeのド阿呆から仕事を手伝って欲しいと言われて、結局引き受けてしまったのが発端です。
何でも、Twitterをポーリングして(30秒おきだがレートリミット残によって間隔を微調整するらしい)特定キーワードを含むツイートをWebSocketで逐次返す機能らしいのですが、最初のHTTPリクエストをトリガーにして処理(TwitterのポーリングとWebSocket)をスタートさせ、ストップを表すHTTPリクエストを受信すると処理を止めて最初のHTTPリクエストのレスポンスを返して完了という処理を実装したそうです。ところが、彼がテスト担当の後輩から30分経つと処理がアボートすると指摘され、何度かテストしてもらったところ100%は再現しない現象だったそうなのですが…いや、冷静に考えてHTTPかTCPのレベルでタイムアウトでしょ?そもそも30分以上もレスポンス返さないなんてどういうWebサービスだよ!JAX-RSの発表を何度もやっているのにどうしてそういう基本的なことに気付かないのかなぁ…
ということがあって、@btnrougeから破格の条件(金品ではないです)を提示されたこともあって、1週間前くらいに一晩で代替品を作ってみました。
先の条件の1つに、作った物はコピーライト=私・BSDライセンスで公開して構わないというのがあったので(新製品のコアを学生に丸投げしてその上オープンソースにされて大丈夫か?という話は置いておいて)、GitHub上のを公開します。
https://github.com/yumix/standstill
プロジェクト名の由来は、ゆかち(井口裕香さん)のシングル・カップリング曲「stand still」です。
§2. 私がJava EE開発の現場から学んだ5のこと
standstillをGitHubに上げた翌日から、@btnrougeが原形をとどめないレベルの改変をしたプル・リクエストをしてくるようになり、その過程でいろいろ学びました。お前プロなのにそんなこともできないの?と思うことから、さすがは腐ってもプロ!と感心することまで、いろいろありました。その中から5つ、取り上げようと思います。
1. ポーリング処理は素直にEJBタイマーを使う
@btnrouge考案のアルゴリズムで最も解せなかったのが、Twitter Search APIのポーリングまわりの処理でした。ソースコードを提示してくれなかったので(ソースコードそのものは社外秘らしい)、あくまで聞いた話に基づきますが、レートリミットをカウントしながら連続でSearch APIを発行し、その後30秒スリープ、もしレートリミットの残りがゼロになったら復活するまでずっとスリープという感じです。
そもそもStreaming APIをなぜ使わなかったのか?という疑問が残るわけですが、Streaming APIだとハッシュタグ以外の日本語キーワードが上手く捉えられず(そこは夏に実験済だとか)、それでは営業的にNGだったのだとか。
TwitterのSearch APIのレートリミットは15分間に180回、1分当たり12回となります。つまりアカウントを独占していてかつ5秒に1回以下の呼び出しであればレートリミットには引っかからないはずです。
私の解決案は、命令キューを用意して、EJBタイマーを使って5秒に1回命令キューから命令を取ってきてそれを実行するというもの。命令としてSearch APIの呼び出しを作っておいて、その命令を取得したらTwitter Search APIを呼び出して結果を返すようにします。命令をキューに入れすぎると捌ききれなくなる恐れはありますが、そこはキューに入れる側で適度にスリープを入れればよろしいかと。とにかく、できるだけ簡単なやり方を採りましょうよ。たぶんそれが一番バグが出にくいと思うから。
私は次のように実装しました。優先順位の異なる2つの命令キューを用意して、そこから命令をフェッチし、実行します。仮に大量のリクエストが来たとしても命令キューが溢れるだけで、Twitterに対する負荷は常にレートリミットの範囲内に収まります。
/** * Cycle of process instructions. */ @Schedule(hour = "*", minute = "*", second = "*/5") private void cycleInBand() { if (fetch(primaryQueue)) { execute(primaryQueue); } else if (fetch(secondaryQueue)) { execute(secondaryQueue); } } /** * Return true when an instruction exists in the queue. * * @param queue the target queue * @return true when an instruction exists in the queue */ private boolean fetch(Dequequeue) { return queue.peek() != null; } /** * Get and execute an instruction from the queue. * * @param queue the target queue * @return the get instruction */ private Instruction execute(Deque queue) { try { Instruction instruction = queue.poll(); if (instruction != null) { System.out.printf("EXE(%03d):[%s]\n", queue.size(), instruction.getClass().getSimpleName().toUpperCase()); instruction.execute(this); } return instruction; } catch (Exception e) { throw new RuntimeException(e); } }
実際のソースコードを見ると分かりますが、2本の命令キューと5秒間隔で呼び出されるメソッドの組とは別に、1本の命令キューと1秒間隔で呼び出されるメソッドが存在しています。こちらはOut of Bandの命令を処理するためのもので、ポーリングを停止させる時にはこちらのキューに動作フラグとなっているレジスタをクリアする命令を投入することで一種の割り込みを実現しています。
@btnrougeの追加実装ではOOB専用にAbortという、通常の命令キューを強制クリアする物騒な命令が追加されていて、ポーリング停止後にAbort命令をするように書き換えられていました。業務上、仕方のない処理なのかもしれませんが、ちょっと怖いですね。
2. 入出力が明確でない場合はジェネリクスとエンコーダ・デコーダを使う
@btnrougeというプログラマは、常日頃から「インプットとアウトプットが確定していない仕様は失格だ」と事あるごとに言っているのですが(←実際にこれは本当ですか?私は彼の普段の仕事ぶりを知らないのでよく分からないのですが)、今回については入力はSearch APIなのでともかく、出力について全く提示されませんでした。何でも 出力インタフェース仕様は社外秘だから出せないとか。普段と言っていること逆じゃん!
と言っているだけでは仕方がないので、ジェネリクスを使った次のようなデコーダのインタフェースを作って、後はそっちで勝手にやれ!ということにしました。
/** * Decoder that convert {@linkplain Status} to actual type. * * @author Yumi Hiraoka - yumix at outlook.com * * @param <E> Actual type of Twitter */ public interface Decoder<E> { /** * Convert {@linkplain Status} to actual type. * * @param status Result of Twitter Search API * @return Tweet as an actual type */ E decode(Status status); }
このようにワンクッション置くことで、よく言えば入出力の抽象化、悪く言えば入出力あいまいな状態での逃げができます。
ちなみに、ジェネリクスを使ったデコーダはWebSocket APIのDecoder.Textインタフェースなどを参考にしました。
アプリケーション自体はJAX-RSを使っているようで、そこまで含めて全部やらないと修正したことにならないと思うのですが、とりあえず検索とポーリングをそれぞれEJBのメソッド呼び出し1回で実現できればよいとのことだったので、その範囲で作りました(例によってWebサービスの仕様も社外秘なのだそうです)。
3. EJBをJARの中に入れる場合はインタフェースを省略しない
これは@btnrougeから聞いて初めて気付いたのですが、JARの中にあるEJBをインジェクションしようとすると上手くいかなくて、ローカル・インタフェースを作っておくと何故か上手くいくそうです(そして実際にローカル・インタフェースを作ったものがプル・リクエストされてきた)。EJBのローカル・インタフェースは省略できるとは言え、疎結合のためにも重要な部分はインタフェースをきちんと用意しておいた方が良さそうです。
4. CDIの@Qualifierと@Producesを積極的に使う
今回、Twitter4Jのオブジェクトを中心に、CDIのプロデューサーを使って出来るだけインジェクションするようにしました(19日向けの試作品がWeld SEでその辺をやるよていなので練習台として)。その時には@Qualifierを付けたアノテーションである限定子を用意して、インジェクションのFromとToをしっかり結びつけます。
例えば、Twitter4JのTwitterは、次のようなプロデューサーを作成してインジェクションできるようにしました。
@ApplicationScoped public class TwitterProducer { @Inject @TwitterAccessToken private AccessToken accessToken; @Produces @TwitterEndpoint public Twitter getTwitter() { Twitter twitter = TwitterFactory.getSingleton(); if (accessToken == null) { System.err.println("accessToken is null"); } twitter.setOAuthAccessToken(accessToken); return twitter; } }
@TwitterEndpointが限定子で、次のように宣言しています。
@Qualifier @Target({TYPE, METHOD, FIELD, PARAMETER}) @Retention(RUNTIME) public @interface TwitterEndpoint { }
そして、そのインジェクション先がこれ。
@Stateless public class TwitterSearchImpl<E> implements TwitterSearch<E> { // snip @Inject @TwitterEndpoint private Twitter twitter; // snip }
インジェクションの対応が明確な場合には限定子を使わなくても大丈夫なようです。ただ私的には、限定子を使うとクラスそのものが持つ意味以上の意味づけもできるので、結構気に入っています。
CDIのプロデューサーに関しては、参照実装Weldのドキュメント、n_agetsumaさんのエントリ、きしださんのエントリを参考にしました。
5. newで作ったオブジェクトの中ではCDIのインジェクションは使えない
これまた@btnrouge経由で分かったことなのですが、newで作成したオブジェクトの中では@Injectは効かないようです。具体的にはSearchクラスとPollクラスの中でTwitterをインジェクションしようとしていたのですが、SearchもPollもnewで作っているのでインジェクションが効かない。@btnrougeから来たプル・リクエストをマージしたら、SearchとPollのコンストラクタ引数でTwitterを渡すことで対応していました。
CDIに頼る場合、オブジェクトの作成は徹頭徹尾CDIに任せておかないといけなくて、newした時点でCDIの管轄から外れてしまうようなのです。私は作りっぱなしだったので全然気付かなかったのですが、Code Fix後のロスタイムで血相変えてコーディングしていたであろう@btnrougeは気付いたようです(そもそもNullPointerExceptionになりますから)。
以上のこと、実はn_agetsumaさんのスライドにははっきり書いてあるんですけどね。
§3. まとめ
今回、他人に代わってJava EEのアプリケーション開発をやることになりましたが、現場では汚い処理がフツーに使われているんだなぁ…と少し残念な思いをしました。あの@btnrougeをしてもかなり泥臭いやり方をしているし(彼の場合は冴えている時とぼんやりしている時の落差が激しいだけとも)、彼の言う「遅れている人たち」はいったいどれほど効率の悪いことをしているのか…彼が加担したという #笑ってはいけないSIer は決して誇張ではなかったのかな、と。
私にも落ち度はたくさんありました。全然テストしないでGitHubに上げて、翌日動かない(でも動くように魔改造した)と連絡を受けたり、理想論を押し通そうとして文句を言われたり(しかも魔改造が済んでいた)、学生の身分としてはいろいろ勉強になりました。良い意味でも悪い意味でも。
私は自分好みのソフトウェアを作れるプログラミングという行為自体は好きだけど、それを仕事にするとなると躊躇います。今回はJava EE 7という好条件でしたが、世の中J2EE 1.4でも新しいとみなされるとよく聞かされているので、そういう世界では生きていく自信がないです。
最後になりましたが、 日付が変わる間際に全然できていないままポストするなど、主催の@kikutaro_さんには大変ご心配をおかけしました。この場を借りてお詫び申し上げます。