Hatena::ブログ(Diary)

CLOVER

2016-05-28

Mavenでの依存ライブラリの最新バージョンを表示、更新したりできるVersions Maven Plugin

JJUG CCC 2016 Springの「Spring Boot で Boot した後に作る Web アプリケーション基盤」というセッションで、Versions Maven Pluginというものがあることを知りました。

Spring Boot で Boot した後に作る Web アプリケーション基盤/spring-boot-application-infrastructure // Speaker Deck

該当のページ。
https://speakerdeck.com/sinsengumi/spring-boot-application-infrastructure?slide=75
https://speakerdeck.com/sinsengumi/spring-boot-application-infrastructure?slide=76

便利そうだな〜と思ったので、メモを兼ねてちょっと試してみることにします。

Versions Maven Plugin - Introduction

プラグインのゴールはこちら。

Versions Maven Plugin - Plugin Documentation

サンプルpom

Mavenプロジェクト(というかpom.xml)がないと話が始まらないので、適当に依存関係やプラグインを適用したpom.xmlを用意してみます。

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.1.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>versions-maven-plugin</artifactId>
                <version>2.1</version>
            </plugin>
        </plugins>
    </build>

全体的に、最新版(2016/5/28時点)より少し古いバージョンを指定しています。

しれっと「versions-maven-plugin」も入れていますが、別に入れていなくてもかまいません。
※Versions Maven Pluginも、少し古いバージョンを指定しています

依存ライブラリの最新バージョンを表示する

依存ライブラリの最新バージョンを表示するには、「versions:display-dependency-updates」を指定します。

$ mvn versions:display-dependency-updates
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building versions-plugin-example 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- versions-maven-plugin:2.1:display-dependency-updates (default-cli) @ versions-plugin-example ---
[INFO] The following dependencies in Dependencies have newer versions:
[INFO]   com.squareup.okhttp3:okhttp ........................... 3.1.0 -> 3.3.0
[INFO]   org.apache.commons:commons-lang3 ........................ 3.3.1 -> 3.4
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.089 s
[INFO] Finished at: 2016-05-28T22:46:18+09:00
[INFO] Final Memory: 10M/201M
[INFO] ------------------------------------------------------------------------

ちなみに、指定されているバージョンがすでに最新版の場合は、結果に表示されなくなります。
※OkHttpのバージョンを上げました

[INFO] The following dependencies in Dependencies have newer versions:
[INFO]   org.apache.commons:commons-lang3 ........................ 3.3.1 -> 3.4

プラグインの最新バージョンを表示する

Mavenプラグインの最新バージョンを表示することもできるようです。「versions:display-plugin-updates」を指定します。

[WARNING] The following plugins do not have their version specified:
[WARNING]   maven-clean-plugin ........................ (from super-pom) 3.0.0
[WARNING]   maven-deploy-plugin ....................... (from super-pom) 2.8.2
[WARNING]   maven-install-plugin ...................... (from super-pom) 2.5.2
[WARNING]   maven-jar-plugin .......................... (from super-pom) 3.0.0
[WARNING]   maven-resources-plugin .................... (from super-pom) 3.0.0
[WARNING]   maven-site-plugin ......................... (from super-pom) 3.5.1
[WARNING]   maven-surefire-plugin .................... (from super-pom) 2.19.1
[INFO] 
[WARNING] Project does not define minimum Maven version, default is: 2.0
[INFO] Plugins require minimum Maven version of: 3.0
[INFO] Note: the super-pom from Maven 3.3.9 defines some of the plugin
[INFO]       versions and may be influencing the plugins required minimum Maven
[INFO]       version.
[INFO] 
[ERROR] Project does not define required minimum version of Maven.
[ERROR] Update the pom.xml to contain
[ERROR]     <prerequisites>
[ERROR]       <maven>3.0</maven>
[ERROR]     </prerequisites>
[INFO] 
[INFO] Require Maven 2.0.2 to use the following plugin updates:
[INFO]   maven-site-plugin ........................................ 2.0-beta-7
[INFO] 
[INFO] Require Maven 2.0.6 to use the following plugin updates:
[INFO]   maven-clean-plugin .............................................. 2.5
[INFO]   maven-deploy-plugin ........................................... 2.8.1
[INFO]   maven-install-plugin .......................................... 2.5.1
[INFO]   maven-jar-plugin ................................................ 2.4
[INFO]   maven-resources-plugin .......................................... 2.6
[INFO]   maven-site-plugin ............................................. 2.0.1
[INFO]   maven-surefire-plugin ......................................... 2.4.3
[INFO] 
[INFO] Require Maven 2.0.9 to use the following plugin updates:
[INFO]   maven-compiler-plugin ........................................... 3.1
[INFO]   maven-surefire-plugin .......................................... 2.17
[INFO] 
[INFO] Require Maven 2.1.0 to use the following plugin updates:
[INFO]   maven-site-plugin ............................................. 2.1.1
[INFO] 
[INFO] Require Maven 2.2.0 to use the following plugin updates:
[INFO]   maven-site-plugin ............................................... 3.0
[INFO] 
[INFO] Require Maven 2.2.1 to use the following plugin updates:
[INFO]   maven-clean-plugin ............................................ 2.6.1
[INFO]   maven-compiler-plugin ........................................... 3.3
[INFO]   maven-deploy-plugin ........................................... 2.8.2
[INFO]   maven-install-plugin .......................................... 2.5.2
[INFO]   maven-jar-plugin ................................................ 2.6
[INFO]   maven-resources-plugin .......................................... 2.7
[INFO]   maven-site-plugin ............................................... 3.4
[INFO]   maven-surefire-plugin ........................................ 2.19.1
[INFO]   org.codehaus.mojo:versions-maven-plugin ......................... 2.2
[INFO] 
[INFO] Require Maven 3.0 to use the following plugin updates:
[INFO]   maven-clean-plugin ............................................ 3.0.0
[INFO]   maven-compiler-plugin ......................................... 3.5.1
[INFO]   maven-jar-plugin .............................................. 3.0.0
[INFO]   maven-resources-plugin ........................................ 3.0.0
[INFO]   maven-site-plugin ............................................. 3.5.1

プラグインの場合は、けっこういろいろ言われます…(なんかエラー出てるし)。

なお、「versions:display-plugin-updates」の場合は、単に使われるプラグインの最新バージョンが表示されるだけで、現在指定してあるのが最新バージョンかどうかは関係がなさそうです。

依存ライブラリをアップデートする

やりたいかどうかはさておき、pom.xmlに定義されている依存ライブラリを最新版にアップデートすることもできます。「versions:use-latest-releases」、「versions:use-latest-versions」を使用します。

「versions:use-latest-releases」の場合。

$ mvn versions:use-latest-releases
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building versions-plugin-example 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- versions-maven-plugin:2.1:use-latest-releases (default-cli) @ versions-plugin-example ---
[INFO] Major version changes allowed
Props: {project.groupId=com.example, project.artifactId=versions-plugin-example, project.version=0.0.1-SNAPSHOT}
[INFO] Updated org.apache.commons:commons-lang3:jar:3.3.1 to version 3.4
Props: {project.groupId=com.example, project.artifactId=versions-plugin-example, project.version=0.0.1-SNAPSHOT}
[INFO] Updated com.squareup.okhttp3:okhttp:jar:3.1.0 to version 3.3.0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.962 s
[INFO] Finished at: 2016-05-28T22:54:30+09:00
[INFO] Final Memory: 10M/201M
[INFO] ------------------------------------------------------------------------

「versions:use-latest-versions」の場合。

$ mvn versions:use-latest-versions
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building versions-plugin-example 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- versions-maven-plugin:2.1:use-latest-versions (default-cli) @ versions-plugin-example ---
[INFO] Major version changes allowed
Props: {project.groupId=com.example, project.artifactId=versions-plugin-example, project.version=0.0.1-SNAPSHOT}
[INFO] Updated org.apache.commons:commons-lang3:jar:3.3.1 to version 3.4
Props: {project.groupId=com.example, project.artifactId=versions-plugin-example, project.version=0.0.1-SNAPSHOT}
[INFO] Updated com.squareup.okhttp3:okhttp:jar:3.1.0 to version 3.3.0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.878 s
[INFO] Finished at: 2016-05-28T22:55:36+09:00
[INFO] Final Memory: 10M/201M
[INFO] ------------------------------------------------------------------------

これで、なんとpom.xmlの内容が書き換わります。

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.3.0</version>
        </dependency>
    </dependencies>

「versions:use-latest-releases」と「versions:use-latest-versions」の違いは、Release版を選ぶかどうかのようですね。

ちなみに、この時pom.xmlのバックアップができていて

$ ll
合計 28
-rw-rw-r--   1 xxxxx xxxxx 1331  528 22:55 pom.xml
-rw-rw-r--   1 xxxxx xxxxx 1333  528 22:54 pom.xml.versionsBackup

「versions:revert」で戻すことができます。

$ mvn versions:revert 
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building versions-plugin-example 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- versions-maven-plugin:2.1:revert (default-cli) @ versions-plugin-example ---
[INFO] Restoring /path/to/pom.xml from /path/to/pom.xml.versionsBackup
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.205 s
[INFO] Finished at: 2016-05-28T23:00:41+09:00
[INFO] Final Memory: 9M/201M
[INFO] ------------------------------------------------------------------------

Revertすると、pom.xml.versionsBackupはなくなります。

反対に、確定する場合は「versions:commit」を利用します。

$ mvn versions:commit

「versions:commit」すると、pom.xml.versionsBackupは削除されます。

使ってみて

全部の機能を試してみたわけではありませんが、Versions Maven Pluginを試してみました。

更新する機能はあまり使わない気もしますが、知っておくとちょっと便利そうだなーと思いました。覚えておきましょう。

Spring SessionのInfinispan向けコードを書いてみた

Spring Session 1.1.0で、Hazelcastのサポートが追加されました。

Spring Session

Spring Session 1.1.0 Released

コードは、このあたり。
https://github.com/spring-projects/spring-session/tree/1.2.0.RELEASE/spring-session/src/main/java/org/springframework/session/hazelcast

このHazelcastのサポートまわりのコードを眺めていて、これなら他のものでもやれそうでは?と思い、Spring SessionのInfinispan版を書いてみました。

https://github.com/kazuhira-r/spring-session-infinispan

まあ、言ってしまうとほぼHazelcastでのコードをInfinispanに書き換えたものなので、まんまと言えばまんまです。

Embedded ModeとClient/Server Mode(Hot Rod)の両方を書いていて、テストコードも(ほぼHazelcast向けコードまんまですが)書いています。一部、Spring Sessionのテストコードから拝借したものも。

有効化のための各アノテーションの定義はこんな感じで、セッションを保持するCacheの名前はとりあえず「springSessions」にしておきました。

Embedded向け。
src/main/java/org/littlewings/spring/session/infinispan/embedded/config/annotation/web/http/EnableInfinispanEmbeddedHttpSession.java

package org.littlewings.spring.session.infinispan.embedded.config.annotation.web.http;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(InfinispanEmbeddedHttpSessionConfiguration.class)
@Configuration
public @interface EnableInfinispanEmbeddedHttpSession {
    int maxInactiveIntervalInSeconds() default 1800;

    String sessionsCacheName() default "springSessions";
}

Client/Server(Hot Rod)向け。
src/main/java/org/littlewings/spring/session/infinispan/remote/config/annotation/web/http/EnableInfinispanRemoteHttpSession.java

package org.littlewings.spring.session.infinispan.remote.config.annotation.web.http;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(InfinispanRemoteHttpSessionConfiguration.class)
@Configuration
public @interface EnableInfinispanRemoteHttpSession {
    int maxInactiveIntervalInSeconds() default 1800;

    String sessionsCacheName() default "springSessions";
}

使う時には、Embedded Modeならinfinispan-coreを、Client/Server Modeなら「infinispan-client-hotrod」を足す感じで。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>8.2.2.Final</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>8.2.2.Final</version>
        </dependency>

Embedded Modeを使うサンプルはこんな感じで、EmbeddedCacheManagerをBean定義します。
※HttpSessionを扱ってるところはテキトーですが

@SpringBootApplication
@EnableInfinispanEmbeddedHttpSession
@RestController
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @RequestMapping("session")
    public Map<String, String> session(HttpSession session) {
        LocalDateTime now = (LocalDateTime) session.getAttribute("now");
        if (now == null) {
            now = LocalDateTime.now();
            session.setAttribute("now", now);
        }

        Map<String, String> response = new HashMap<>();
        response.put("isNew", Boolean.toString(session.isNew()));
        response.put("now", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        return response;
    }

    @Bean
    public EmbeddedCacheManager embeddedCacheManager() {
        EmbeddedCacheManager cacheManager =
                new DefaultCacheManager(new GlobalConfigurationBuilder().transport().defaultTransport().build());
        cacheManager
                .defineConfiguration("springSessions",
                        new org.infinispan.configuration.cache.ConfigurationBuilder().clustering().cacheMode(CacheMode.DIST_SYNC).build());
        return cacheManager;
    }
}

Client/Server Modeならこんな感じ。RemoteCacheManagerをBean定義します。Client/Server Modeの場合は、あらかじめServer側にCacheを定義しておく必要がありますが…。

@SpringBootApplication
@EnableInfinispanRemoteHttpSession
@RestController
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @RequestMapping("session")
    public Map<String, String> session(HttpSession session) {
        LocalDateTime now = (LocalDateTime) session.getAttribute("now");
        if (now == null) {
            now = LocalDateTime.now();
            session.setAttribute("now", now);
        }

        Map<String, String> response = new HashMap<>();
        response.put("isNew", Boolean.toString(session.isNew()));
        response.put("now", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        return response;
    }

    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new org.infinispan.client.hotrod.configuration.ConfigurationBuilder()
                        .addServers("localhost:11222")
                        .build());
    }
}

作ってみて

Spring SessionにMapSessionRepositoryというものがあって、こちらを使うとMapベースのものならけっこう簡単に作れるんだなーということがわかりました。
https://github.com/spring-projects/spring-session/blob/1.2.0.RELEASE/spring-session/src/main/java/org/springframework/session/MapSessionRepository.java

まあ、ひととおりプログラム書いて、テストコード書いて、サンプル書いて試してみたところで、初めてSpring Sessionを使ったコードを書いたことにふと気付きましたが…。

気になったこと、悩んだこと

ExpiredEvent発火のタイミング

Infinispanのexpirationのデフォルト設定だと、wake-up-intervalが1分になっていて、expireこそするもののExpiredEventがすぐに発火せずにテストコードではこの値を縮める必要がありました…。

Cluster Listener

Client/Server ModeとEmbedded Mode(Distributed Cache)で、イベントを受け取るNodeの数が違うことがあるんじゃないかなぁと。最初は対称性を取ろうとして、Embedded Modeの場合はCluster Listenerにして全Nodeがイベントを受け取るようにした方がいいんじゃないかな?と思ったのですが、どうなのでしょう。

Spring Sessionの他の実装を見ていると、Client/Server Modeのものかそうでないものか(Distributed/Partitionedなものを使っている場合)で、イベントを受け取るNode数が変わるような気も…。

今回は、通常のListenerにしてDistributed Cacheの時はエントリのOwnerのNodeがイベントを受け取るようになっています。

Eviction

EvictedEventの扱いは、悩みどころでした。Persistenceを考えると、Evictされる=Removeされるは必ずしも成立しないですし。ただ、Client/Server ModeのListenerはEvictedEventをサポートしていないので、今回はEmbedded Modeの場合も外しておきました。

なお、HazelcastのListenerがExpireとEvictをそう区別していないことに、ここで気付きましたが…。

とりあえず、最低ラインは作れたので満足しました。あとは他のSpring Sessionの実装を眺めて、気ままにいじっていこうかなと思います。

InfinispanはWildFlyのセッション(クラスタ可)で使われていますが、他のグリッドとかと違ってServletFilterとかでの実装は提供してないんですよね。なので、ちょっと作ってみたいなぁとは思っていましたが、ServletFilterまで頑張る気にはちょっとなれず。そこにSpring Sessionがあったのでやってみました的な。

まあ、今回は興味本位です。

2016-05-25

Java Day Tokyo 2016に参加してきました

5/24に、東京マリオネットホテルで開催されたJava Day Tokyo 2016に参加してきました。

Java Day Tokyo 2016|日本オラクル

f:id:Kazuhira:20160524121953j:image

f:id:Kazuhira:20160524090840j:image

f:id:Kazuhira:20160524092234j:image

平日ですが、今回は会社的に研修扱いということで、堂々と行ってきました。私服ですけれど。

Java Day Tokyoは、初めての参加です。行ったことない場所だったので、若干迷いながらなんとか会場へ。

参加したセッション

今回参加したセッションは、こちらです。

[Keynote] Innovate, Collaborate, with Java

複数の登壇者が、これからのJavaのポイントやIoT、クラウドまわりの話を中心に行ったり、デモをしたりしていました。ドローンとPepperが出てきたのには、ちょっと驚きましたね。損害保険ジャパンさんのJava EEへの移行の話もありました。それにしても、次のJava EEは登場がいつになるのでしょう…。

[1-C] ゴールドマン・サックスオープンソースへの取り組み

ゴールドマン・サックスの伊藤 博志さんによるセッション。ユーザー企業としては一風変わっていて、OSSの活用や貢献でいろいろ活動をしているという話でした。Elasticsearchを独自にビルドして運用していたり、GS Collections/Eclipse Collectionsの話は今回初めてまともに聞いたので、新鮮でした。Elasticsearchをかなり大規模に運用していて、その数字を見て驚きましたね。

[2-C] Java EE 7アプリケーションとWebセキュリティ

株式会社FCSの浦上 太一さんのセッション。Java EEを使ってのセキュリティの取り組みということで、社内システム中心でXSS対策を中心に考えていた頃から、公開するシステムを作ってそこからセキュリティ対策と向き合うようになったというお話。自分はJSPJSFをほとんど使っていないので、XSSをこの両仕様の範囲でどう対策するかや、JPAでの対策などが聞けてよかったなぁと思います。とても実践的でした。ただ、EEでカバーしきれない範囲もけっこうあったので、そのあたりは説明がちょっとつらそう?と思いました。このあたりは、IPAや書籍などを参考にしつつ、地道にやっていくしかないですよねぇ。

[3-C] Java Concurrency, A(nother) Peek Under the Hood

日本オラクル株式会社のデイビッド・バックさんによるセッション。書籍「Java Concurrency in Practice」の内容をプレゼンにしたような内容で、複数スレッドにおけるJavaのメモリモデルの注意点や、Client VMとServer VMのJITコンパイル後の挙動の差をデモで示したりと、あまり人から聞けないタイプの発表だったのでかなり面白かったです。聞けてよかったセッションのひとつでした。

[4-E] 実践して分かったJavaマイクロサービス開発

Acroquest Technology株式会社の谷本 心さんによるセッション。アプリケーションを順を追ってマイクロサービス化していった時にうまくいったことや、悩んだことを実際の業務で得た経験からお話しされていて、生々しい話としてはとても面白い話だったと思います。自分がアプリケーションをマイクロサービスとして開発することになるかどうかは微妙なところですが、気にするポイントやマイクロサービスを支えるテクノロジーというのはやっぱりある程度押さえていた方がよいと思うので、勉強しなくちゃなぁと思いました。
あと、個人的にはHazelcastの話がちょっと聞きたかったです。

[5-A] 実システムのためのJava EE 7

楽天株式会社の岩崎 浩文さんによるセッション。Java EE 7以前のJava EEJ2EEアプリケーションから、Java EE 7へマイグレーションする際の注意点などが主な内容でした。これはこれでアップグレードを伴う移行としては参考になるのでしょうけれど、個人的には実際にJava EEを適用するにあたっての苦労した点やポイントなどが聞けたらよかったなぁと思いました。

[Night] パネル・ディスカッション - Java Day Night Session with NightHacking Tour

イベント最後のプログラム。4名のパネラーが、JCP、コミュニティについて話していました。途中で、ゲームがまわってきてマリオをする羽目になりましたが…。終演後は、みなさんで写真をパシャリ。
写真に写るかどうかは若干迷いましたが、一声かかって乗ってみることにしました。いい思い出になったと思います。とりあえず、Twitterで公開されていた写真は保存しておきました。

閉会してから

品川駅付近で、10名ほどで2次会的なものへ。ここでもお初にお話しする方がいらっしゃいましたし、乾杯を振られたりでめっちゃドキドキしていました…。とはいえ、いろんな話ができて楽しかったです。

社内の方がこのJava Day Tokyoで発表するということがきっかけで今回初めて参加しましたけれど、CCCとはまた違った雰囲気で参加できてよかったなーと思いました。
平日開催だと次の機会に参加できるかどうかは微妙なところがある気がしますが、できればまた参加できたらいいなぁと思います。

運営の方々、発表された方々、会場でお会いした方々、ありがとうございました!!

オマケ

自分がなぜか他の方から会場で簡単に発見されてしまうらしくて、見つけやすいツチノコという烙印を押された?気がします…。

いや、ツチノコかどうかはいいんですけど、ひっそりと生きてきたいとは思うので、ステルス性能を高めたいなーと思いました。

2016-05-22

JJUG CCC 2016 Springに参加してきました

5/21にベルサール新宿グランドで行われた、JJUG CCC 2016 Springに参加してきました。

JJUG CCC 2016 Spring | 日本Javaユーザーグループ

タイムテーブルは、こちら。

Timetable JJUG CCC 2016 Spring | 日本Javaユーザーグループ

ホントは9時30分から行く気満載だったのですが、起きたら9時だったので10時ちょいから参加してました…。
先週インフルエンザにかかって、体力がロストしていたようです…。

参加したセッション

今回参加したセッションは、こちらです。

E-1 テスト自動化のまわりみち

@irofさんのセッション。寝坊したので、ちょっと遅れて参加です。自分のところではテスト自動化ってほとんどできていないので、それに向けてどう取り組んでいくか、どう進めていったらいいだろうということが話されていて、とても参考になりました。あと、基本的なテストの知識について、やっぱ押さえてないとなぁとも思わされたセッションでした。

F-2 Thymeleaf 3 を使ってみよう!

@bufferingsさんによる、ThymeleafとThymeleaf 3.0の紹介セッション。自分はThymeleafは使ったことがないのですが、このセッションは入門としてはとてもわかりやすかったです。今回の資料を見れば、Thymeleafの基本的な使い方はわかりそうな気がします。個人的には、Thymeleaf 3.0のText Modeに期待です。

E-3 Spring Boot で Boot した後に作る Web アプリケーション基盤

吉田 朋也さんによるセッション。Spring Bootでとりあえず起動してみた後に、じゃあどうやって開発できる状態までもっていこうか?そこに至るまでに考えることってなに?という内容でしたね。共感できる内容でもありましたし、実現方法として知らないこともあったので、参考になりました。

E-4 テストゼロからイチへ進むための戦略と戦術

@nabedgeさんによるセッション。最初は他のセッションを見ようとしていたのですが、途中で部屋を移っての参加です。やっぱり、テストを始めるにあたってのセッションは参考になります。

AB-5 Apache Apexとインメモリー最適化による超高速処理の世界

ウルシステムズの漆原 茂さんによるセッション。Apache Apexの紹介と自社のメモリー圧縮製品に関するセッションでしたが、今回のCCCのセッションで1番笑いました。積極的に笑いを取るスタイルでしたね。Apache Apexは面白そうなんですけど、YARNかぁ、YARNかぁという感じでした…(個人にはツライ)。

AB-6 ビッグデータじゃなくても使えるSpark☆Streaming

島本 多可子さんによるセッション。Spark Streamingを大規模環境でなくてもどう使っていったのか?という話で、Stanalone Clusterで小さく使っていらっしゃるみたいです。こういう話を聞くと、ちょっと身近なところでも試してみたくなりますね…。あと、やっぱりStreaming処理をしているアプリケーションのメンテナンスは考えておかないといけないんだなぁと…。

CD-7 マイクロフレームワーク enkan(とkotowari)ではじめるREPL

@kawasimaさんのセッション。なんか、行く前から聞くことになっていたらしいです(?)。
※その割には、遅れて参加…

はい。

懇親会と

セッション中、それから懇親会の会場では、また新しく初めてお話できた方ができまして、また自分を探していただいた方もいらっしゃいまして、とてもありがたい話だなぁと思いました。

ただ、事あるごとにツチノコ感が薄れていると言われたり、職に関する話で詰め寄られたり(?)してたので、自分の今後のキャラクターイメージに不安を覚えたりもしてました…。恐ろしい…。

あと、今回初めて名刺を持ってきました。少なめにしか持っていないのを完全に忘れていて足りなくならないかちょっとドキドキしていましたが、とりあえず今回渡しておかないとなーと思っていた方には渡せたので大丈夫でした。

2次会

懇親会後は、終電付近まで2次会に参加。@kisさん、@nobeansさん、@yy_yankさんと同じテーブルで話していました。

@kisさんの話が謎解きのようでした…。しかも、@kisさんと話すこと自体初めてだったという…。

まとめ

今回で3回目のCCCへの参加でしたが、やっぱり楽しかったですね。ご挨拶できた方も増えましたし。今回ご挨拶できなかった方は、ぜひともまた次回に…。
自分からも、もうちょっと声掛けにいかないとですね。

また是非とも参加したいです。

お会いした方々、セッションを行われた方々、運営の方々、ありがとうございました!

2016-05-19

JCache(Hazelcast Client)で、独自のクラスをCacheに突っ込むとClassNotFoundになるの?という話

今日、JCacheについて、こんな質問を受けまして。

「Hazelcast ClientをJCacheの実装に使って値に独自のクラスを入れる時、サーバー側にも独自のクラスをデプロイしないといけないの?」

なんでこんなことを聞かれたかって、実際にやってみたらサーバー側で「クラスが見つからない」とエラーになったと言います。

ただ、Client/Server構成のグリッドの使い方から考えると、これでエラーになるのはちょっと不便なので、何が起こっているのか調べてみることにしました。

というわけでコード的にはJCacheの話ですが、内容としてはHazelcast ClientをJCacheの実装として採用したケースの話になりますのでその点についてはご注意ください。

準備

Client/Server構成となるので、両方のプログラムが必要です。

Server側は非常に単純に作ったので、あとで載せます。

Client側はふつうに作るので、まずはMaven依存関係から記載。

        <!-- JCache -->
        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast-client</artifactId>
            <version>3.6.2</version>
        </dependency>

        <!-- for Testing -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.4.1</version>
            <scope>test</scope>
        </dependency>

JCache APIと、JCacheの実装としてHazelcast Clientを追加。Hazelcastの場合、Hazelcast Clientがクラスパス上に存在していた場合、デフォルトでClientとして起動します。

独自のクラスを作成

なんでもいいので、単純なSerializableなクラスを作ります。今回は、こんなのにしました。
src/main/java/org/littlewings/jcache/hazelcast/MyValueClass.java

package org.littlewings.jcache.hazelcast;

import java.io.Serializable;

public class MyValueClass implements Serializable {
    private static final long serialVersionUID = 1L;

    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

はい。

Server側

ここで、Server側を。こちらは、Groovyで超簡易に作成しました。
server.groovy

@Grab('javax.cache:cache-api:1.0.0')
@Grab('com.hazelcast:hazelcast:3.6.2')
import com.hazelcast.core.Hazelcast

def hazelcast = Hazelcast.newHazelcastInstance()

System.console().readLine("> Enter stop Hazelcast server.")

hazelcast.lifecycleService.shutdown()

これだけです。

とりあえず、このServerに起動していてもらいます。クラスパスなどには、一切追加は行いません。

$ groovy server.groovy
5 19, 2016 9:46:46 午後 com.hazelcast.config.XmlConfigLocator
情報: Loading 'hazelcast-default.xml' from classpath.
5 19, 2016 9:46:46 午後 com.hazelcast.instance.DefaultAddressPicker
情報: [LOCAL] [dev] [3.6.2] Prefer IPv4 stack is true.
5 19, 2016 9:46:46 午後 com.hazelcast.instance.DefaultAddressPicker
情報: [LOCAL] [dev] [3.6.2] Picked Address[172.17.0.1]:5701, using socket ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=5701], bind any local is true
5 19, 2016 9:46:46 午後 com.hazelcast.system
情報: [172.17.0.1]:5701 [dev] [3.6.2] Hazelcast 3.6.2 (20160405 - 0f88699) starting at Address[172.17.0.1]:5701
5 19, 2016 9:46:46 午後 com.hazelcast.system
情報: [172.17.0.1]:5701 [dev] [3.6.2] Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
5 19, 2016 9:46:46 午後 com.hazelcast.system
情報: [172.17.0.1]:5701 [dev] [3.6.2] Configured Hazelcast Serialization version : 1
5 19, 2016 9:46:46 午後 com.hazelcast.spi.OperationService
情報: [172.17.0.1]:5701 [dev] [3.6.2] Backpressure is disabled
5 19, 2016 9:46:46 午後 com.hazelcast.spi.impl.operationexecutor.classic.ClassicOperationExecutor
情報: [172.17.0.1]:5701 [dev] [3.6.2] Starting with 4 generic operation threads and 8 partition operation threads.
5 19, 2016 9:46:47 午後 com.hazelcast.instance.Node
情報: [172.17.0.1]:5701 [dev] [3.6.2] Creating MulticastJoiner
5 19, 2016 9:46:47 午後 com.hazelcast.core.LifecycleService
情報: [172.17.0.1]:5701 [dev] [3.6.2] Address[172.17.0.1]:5701 is STARTING
5 19, 2016 9:46:47 午後 com.hazelcast.nio.tcp.nonblocking.NonBlockingIOThreadingModel
情報: [172.17.0.1]:5701 [dev] [3.6.2] TcpIpConnectionManager configured with Non Blocking IO-threading model: 3 input threads and 3 output threads
5 19, 2016 9:46:49 午後 com.hazelcast.cluster.impl.MulticastJoiner
情報: [172.17.0.1]:5701 [dev] [3.6.2] 


Members [1] {
	Member [172.17.0.1]:5701 this
}

5 19, 2016 9:46:49 午後 com.hazelcast.core.LifecycleService
情報: [172.17.0.1]:5701 [dev] [3.6.2] Address[172.17.0.1]:5701 is STARTED
> Enter stop Hazelcast server.

テストコード

それでは、このServerに対してJCacheのAPIでアクセスするテストコードを書きます。

src/test/java/org/littlewings/jcache/hazelcast/CacheClientTest.java

package org.littlewings.jcache.hazelcast;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.Configuration;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;

import com.hazelcast.nio.serialization.HazelcastSerializationException;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class CacheClientTest {
    // ここに、テストを書く!
}

まず最初に、JCache APIを真っ当に使って書いてみます。だいたいこんな感じのコードになると思います。

    @Test
    public void failedCreateCacheWithMyDefinedClass() {
        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager()) {
            Configuration<String, MyValueClass> configuration =
                    new MutableConfiguration<String, MyValueClass>()
                    .setTypes(String.class, MyValueClass.class);

            assertThatThrownBy(() -> manager.createCache("mySimpleCache", configuration))
                    .isInstanceOf(HazelcastSerializationException.class)
                    .hasMessage("java.lang.ClassNotFoundException: org.littlewings.jcache.hazelcast.MyValueClass");
        }
    }

ところが、テストコードが示しているように、このコードは実行に失敗します。しかも、シリアライズのエラー、その原因としてClassNotFoundExceptionと言われるわけです。

この時、スタックトレースとしてはこのような状態になります。

com.hazelcast.nio.serialization.HazelcastSerializationException: java.lang.ClassNotFoundException: org.littlewings.jcache.hazelcast.MyValueClass

	at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$ClassSerializer.read(JavaDefaultSerializers.java:181)
	at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$ClassSerializer.read(JavaDefaultSerializers.java:169)
	at com.hazelcast.internal.serialization.impl.StreamSerializerAdapter.read(StreamSerializerAdapter.java:46)
	at com.hazelcast.internal.serialization.impl.AbstractSerializationService.readObject(AbstractSerializationService.java:214)
	at com.hazelcast.internal.serialization.impl.ByteArrayObjectDataInput.readObject(ByteArrayObjectDataInput.java:600)
	at com.hazelcast.config.CacheConfig.readData(CacheConfig.java:543)
	at com.hazelcast.internal.serialization.impl.DataSerializer.read(DataSerializer.java:121)
	at com.hazelcast.internal.serialization.impl.DataSerializer.read(DataSerializer.java:47)
	at com.hazelcast.internal.serialization.impl.StreamSerializerAdapter.read(StreamSerializerAdapter.java:46)
	at com.hazelcast.internal.serialization.impl.AbstractSerializationService.toObject(AbstractSerializationService.java:170)
	at com.hazelcast.spi.impl.NodeEngineImpl.toObject(NodeEngineImpl.java:234)
	at com.hazelcast.client.impl.protocol.task.cache.CacheCreateConfigMessageTask.prepareOperation(CacheCreateConfigMessageTask.java:46)
	at com.hazelcast.client.impl.protocol.task.AbstractPartitionMessageTask.processMessage(AbstractPartitionMessageTask.java:58)
	at com.hazelcast.client.impl.protocol.task.AbstractMessageTask.initializeAndProcessMessage(AbstractMessageTask.java:118)
	at com.hazelcast.client.impl.protocol.task.AbstractMessageTask.run(AbstractMessageTask.java:98)
	at com.hazelcast.spi.impl.operationservice.impl.OperationRunnerImpl.run(OperationRunnerImpl.java:127)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.processPartitionSpecificRunnable(OperationThread.java:159)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.process(OperationThread.java:142)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.doRun(OperationThread.java:124)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.run(OperationThread.java:99)
	at ------ End remote and begin local stack-trace ------.(Unknown Source)
	at com.hazelcast.client.spi.impl.ClientInvocationFuture.resolveResponse(ClientInvocationFuture.java:128)
	at com.hazelcast.client.spi.impl.ClientInvocationFuture.get(ClientInvocationFuture.java:95)
	at com.hazelcast.client.spi.impl.ClientInvocationFuture.get(ClientInvocationFuture.java:74)
	at com.hazelcast.client.spi.impl.ClientInvocationFuture.get(ClientInvocationFuture.java:37)
	at com.hazelcast.client.cache.impl.HazelcastClientCacheManager.createConfig(HazelcastClientCacheManager.java:188)
	at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.createCacheInternal(AbstractHazelcastCacheManager.java:116)
	at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.createCache(AbstractHazelcastCacheManager.java:145)
	at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.createCache(AbstractHazelcastCacheManager.java:63)
	at org.littlewings.jcache.hazelcast.CacheClientTest.failedCreateCacheWithMyDefinedClass(CacheClientTest.java:20)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.ClassNotFoundException
	at com.hazelcast.client.impl.protocol.ClientExceptionFactory$12.createException(ClientExceptionFactory.java:168)
	at com.hazelcast.client.impl.protocol.ClientExceptionFactory.createException(ClientExceptionFactory.java:613)
	at com.hazelcast.client.impl.protocol.ClientExceptionFactory.createException(ClientExceptionFactory.java:580)
	at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.handleClientMessage(ClientInvocationServiceSupport.java:314)
	at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.process(ClientInvocationServiceSupport.java:296)
	at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.doRun(ClientInvocationServiceSupport.java:289)
	at com.hazelcast.client.spi.impl.ClientInvocationServiceSupport$ResponseThread.run(ClientInvocationServiceSupport.java:266)

ちなみにこれ、以下の部分より上はServer側で生成されたスタックトレースです。

	at ------ End remote and begin local stack-trace ------.(Unknown Source)

その内容が、Client側にも現れます。この時、Server側は一切エラーを吐きません。

なので、ClientとServerのスタックトレースが交互に現れた状態になっています。で、よく見るとCacheManager#createCacheでコケているようですね。

なにが起こったのか?

これで、なにが起こっているのかを説明しておきます。

CacheManager#createCacheを呼び出した時、Hazelcast ClientによるJCache実装は、その定義内容をServer側へシリアライズして送信します。

            Data cacheConfigData = clientContext.getSerializationService().toData(config);

https://github.com/hazelcast/hazelcast/blob/v3.6.2/hazelcast-client/src/main/java/com/hazelcast/client/cache/impl/HazelcastClientCacheManager.java#L182

            Future<ClientMessage> future = clientInvocation.invoke();
            if (syncCreate) {
                final ClientMessage response = future.get();

https://github.com/hazelcast/hazelcast/blob/v3.6.2/hazelcast-client/src/main/java/com/hazelcast/client/cache/impl/HazelcastClientCacheManager.java#L186-L188

Server側は、この内容を受け取りCacheを定義しようとします。

この時、定義内容をデシリアライズしようとするわけですが、送信内容にMutableConfiguration#setTypesした時のClassクラスが含まれています。

            Configuration<String, MyValueClass> configuration =
                    new MutableConfiguration<String, MyValueClass>()
                    .setTypes(String.class, MyValueClass.class);

今回は、MyValueClassというクラスです。

で、これをServer側でデシリアライズする時にClassクラスをロードしちゃうわけですね。

        //SUPER
        keyType = in.readObject();
        valueType = in.readObject();

https://github.com/hazelcast/hazelcast/blob/v3.6.2/hazelcast/src/main/java/com/hazelcast/config/CacheConfig.java#L541-L543

結果、ClassNotFoundExceptionとなるわけです。Client側には、シリアライズ時の処理で失敗したということで、まとめて報告されることになります。

回避方法を考える

というわけで、このままでは使えません。

もちろんServer側のクラスパスに自作のクラスを加えれば問題は解消できますが、Client/Server構成であることを考えると、その案は却下としたいところです。

Client/Server構成だからといって、CacheManager#createCacheせずに、いきなりCacheManager#getCacheしてみても、これはうまくいきません。

    @Test
    public void notCreateCacheIsNull() {
        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             Cache<String, MyValueClass> cache = manager.getCache("mySimpleCache")) {
            assertThat(cache).isNull();  // CacheManager#createCacheしない場合は、null
        }
    }

多くのJCacheのCacheManagerの実装は、createCacheした時のCacheのインスタンスを内部的にMapで持っていることが多いみたいで、Hazelcastもそのひとつになります。で、Client側のCacheManagerにその定義が見つからない、と…。

というわけで、CacheManager#createCacheは呼ぶ必要があるわけです。

となると、まあ泥縄的な案しかないわけですが。

結果としては、MutableConfiguration#setTypesで独自の型を指定するのをやめると回避することができます。。

    @Test
    public void workaround() {
        Configuration<String, MyValueClass> configuration =
                new MutableConfiguration<String, MyValueClass>();
                // .setTypes(String.class, MyValueClass.class);  // <- 削除

        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             Cache<String, MyValueClass> cache = manager.createCache("mySimpleCache", configuration)) {
            MyValueClass value = new MyValueClass();
            value.setValue("Hello JCache!!");

            cache.put("key1", value);

            assertThat(cache.get("key1").getValue())
                    .isEqualTo("Hello JCache!!");

            cache.remove("key1");
            assertThat(cache.containsKey("key1")).isFalse();
        }
    }

この場合、キーと値の型にObjectを指定したのと同義になります。

もしくは、Java SE標準APIの範囲でならsetTypesしてもいいですが…。

一応、この実装方法をとってもCache自体はタイプセーフを保つことはできます。

そもそもMutableConfiguration#setTypesで指定した型をいつ使うかですが、CacheManager#getCacheする時に追加の引数として使うもので(取得しようとしているCache定義の検証として)、指定しなくても動かないなんてことはありません。

ですが、極力指定すべきだとは思いますけどねぇ…。

かなり実装を透かして見た感じになりますが、ワークアラウンドでした。

ところで

他のClient/Server構成を取れる製品だと、どうなのでしょう?

Infinispanでは、MUtableConfiguration#setTypesで独自の型を指定しても、Serverへのデプロイなどなしのままで問題なく動きました。

Apache Igniteは…Client/Serverで動かしたことがありません(誰かお願いします)。

まとめ

というわけで、Hazelcast ClientをJCache実装に採用した場合に、シリアライズでエラーになるケースのひとつ(他は知りませんが)の回避策っぽいものを書いてみました。

が、個人的にはちょっと微妙です。MutableConfiguration#setTypesでハマるとは、たぶん使う側は考えないでしょうし、ここは本来指定すべきだと思いますからねぇ…。

2016-05-17

HazelcastのLite Membersを試す

Hazelcast 3.6から、Lite Membersという機能が追加されています。

Lite Members: With the re-introduction of Hazelcast Lite Members (it was removed starting with Hazelcast 3.0 release), you are able to specify certain members in your cluster so that they do not store data. You can use these lite members mostly for your task executions and listener registrations. Please refer to Enabling Lite Members.

http://docs.hazelcast.org/docs/release-notes/index.html#36

Enabling Lite Members

Lite Membersというのは、Server構成を取ってクラスタを構成しつつも、データを持たないMemberとしてクラスタに参加できる機能なようです。

Release Notesを見ていると、3.0のリリース時に削除されたものっぽいですねぇ…。

これができると何が嬉しいかということですが、ドキュメントによるとタスクを実行したり、Listenerを登録するMemberとして利用されることを想定しているみたいですね。

でも、どうなのでしょう?分散タスク/Aggregationsを実行する時にはData Localityの都合を考えると、それほど嬉しくないのでは?という気がしないでも。タスクをキックするだけ、という割り切りならよいかもしれません。

Listenerは好例でしょうね。

あとは、Client/Server構成時だと制限を受けるような時に、利用できると嬉しいかもですね(あんまりClient/Server構成と、Serverのみの構成時のメリット・デメリット覚えてませんけど)。

まあ、とりあえず使ってみましょう。

準備

Maven依存関係は、以下のように定義。

        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.6.2</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.4.1</version>
            <scope>test</scope>
        </dependency>

とりあえず、hazelcastのみを利用します。あとは、テストコード用にJUnitとAssertJです。

テストコードの雛形

テストを実行するための、雛形コードを以下に。
src/test/java/org/littlewings/hazelcast/litemember/LiteMemberTest.java

package org.littlewings.hazelcast.litemember;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.Partition;
import com.hazelcast.partition.NoDataMemberInClusterException;
import org.assertj.core.data.MapEntry;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class LiteMemberTest {
    // ここに、テストを書く!
}

Hazelcastを使った簡易なクラスタ構成のためのメソッドなども用意するのですが、これは順次記載していきます。

Lite Membersのみでプログラムを実行する

最初は、Lite Memberのみ(データを持たないMemberのみ)でクラスタを構成してみます。

Lite Memberを作成するためのメソッドとして、以下を用意。

    protected void withLiteMember(Consumer<HazelcastInstance> consumer) {
        Config config = new Config();
        config.setLiteMember(true);

        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(config);
        try {
            consumer.accept(hazelcast);
        } finally {
            hazelcast.getLifecycleService().shutdown();
        }
    }

通常と違うところは、Config#setLiteMemberにtrueを設定していることだけです。これで、Lite Memberという扱いになります。

テストコードはこちら。

    @Test
    public void standaloneLiteMember() {
        withLiteMember(hazelcast -> {
            Map<String, String> map = hazelcast.getMap("default");
            assertThatThrownBy(() -> map.put("key", "value"))
                    .isInstanceOf(NoDataMemberInClusterException.class)
                    .hasMessage("Partitions can't be assigned since all nodes in the cluster are lite members");
        });
    }

検証コードが示していることから明らかかもですが、このコードはHazelcastのDistributed Mapにデータをputする時に失敗します。

データを持てるMemberが、ひとりもいないからですね。

というわけで、少なくともデータを持てるMemberを追加する必要がありそうです。

Lite Member+通常のMember

では、ひとつMemberを増やしてクラスタを構成してみましょう。

通常のHazelcastのMemberを任意数起動する、ヘルパーメソッドを用意します。

    protected void withHazelcast(Consumer<HazelcastInstance> consumer) {
        withHazelcast(1, consumer);
    }

    protected void withHazelcast(int numInstances, Consumer<HazelcastInstance> consumer) {
        List<HazelcastInstance> hazelcastInstances = IntStream
                .rangeClosed(1, numInstances)
                .mapToObj(i -> Hazelcast.newHazelcastInstance(new Config()))
                .collect(Collectors.toList());

        hazelcastInstances.forEach(h -> h.getLifecycleService().shutdown());
    }

こちらと、先ほどのLite Member用のメソッドを組み合わせて、テストコードを作成します。最初に通常のMemberを起動して、その後にLite Memberを起動するようにしました。

    @Test
    public void liteMemberWithNormalMember() {
        withHazelcast(hasDataHazelcast -> {
            withLiteMember(liteHazelcast -> {
                Map<String, String> map = liteHazelcast.getMap("default");
                map.put("key", "value");

                assertThat(map)
                        .containsExactly(MapEntry.entry("key", "value"));

                assertThat(liteHazelcast.getConfig().isLiteMember()).isTrue();

                assertThat(liteHazelcast.getCluster().getMembers()).hasSize(2);

                Set<Partition> partitions = liteHazelcast.getPartitionService().getPartitions();
                assertThat(partitions).hasSize(271);
                assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(1);

                Partition partition = partitions.stream().findAny().get();
                assertThat(partition.getOwner())
                        .isEqualTo(hasDataHazelcast.getCluster().getLocalMember())
                        .isNotEqualTo(liteHazelcast.getCluster().getLocalMember());
            });
        });
    }

この構成だと、データのputが可能になります。

Lite Memberと設定したMember自体は、そのように構成されていることが確認できます。

                assertThat(liteHazelcast.getConfig().isLiteMember()).isTrue();

また、Hazelcast内部のPartition数はデフォルトの271ですが、オーナーがひとつに偏っていることが確認できます。

                Set<Partition> partitions = liteHazelcast.getPartitionService().getPartitions();
                assertThat(partitions).hasSize(271);
                assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(1);

                Partition partition = partitions.stream().findAny().get();
                assertThat(partition.getOwner())
                        .isEqualTo(hasDataHazelcast.getCluster().getLocalMember())
                        .isNotEqualTo(liteHazelcast.getCluster().getLocalMember());

この時、普段見るHazelcastのクラスタ構成とは異なり、クラスタ構成時にMemberが複数現れません。

Members [1] {
	Member [172.17.0.1]:5701 this
}

Lite Member分が現れていない、という方が正確な気がしますが。ポートのリッスンも行われていないようです。

ただ、クラスタ内のMember数は「2」だと言いますが。

                assertThat(liteHazelcast.getCluster().getMembers()).hasSize(2);

なお、先ほどのLite Memberのみで起動した時はさすがにどうにもならないのか(?)、5701ポートでのリッスンとMembersの表示が行われますが。

通常のクラスタ構成時と比較してみる

一応、通常のクラスタ構成時にどうなるか、載せておきましょう。この構成では、Lite Memberはいません。

    @Test
    public void normalHazelcastCluster() {
        withHazelcast(2, hazelcast -> {
            Map<String, String> map = hazelcast.getMap("default");
            map.put("key", "value");

            assertThat(map)
                    .containsExactly(MapEntry.entry("key", "value"));

            assertThat(hazelcast.getConfig().isLiteMember()).isFalse();

            Set<Partition> partitions = hazelcast.getPartitionService().getPartitions();
            assertThat(partitions).hasSize(271);
            assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(2);
        });
    }

distinctをとっても、結果が2になりますね。この構成だと、両Memberがデータを持てるからです。

少し内部的な

ここで、少しHazelcast内でどうなっているか見てみましょう。

サラッと見た感じ(と挙動から)、データを扱ったりPartitionについて見るときには、データを持てるMemberでのみ見ているみたいですね。
https://github.com/hazelcast/hazelcast/blob/v3.6.2/hazelcast/src/main/java/com/hazelcast/partition/impl/InternalPartitionServiceImpl.java#L383-L386

絞り込みは、こちらのクラスを使って行われる模様。
https://github.com/hazelcast/hazelcast/blob/v3.6.2/hazelcast/src/main/java/com/hazelcast/cluster/memberselector/MemberSelectors.java

Lite Memberの判定を使っている箇所は、それほど多くありません。NodeやMemberなどに属性として保持されていますが、実際に条件分岐などに利用されているのは少なめです。


設定ファイルからLite Memberを構成する

最後に、Lite Memberを設定ファイルで構成してみたいと思います。

ヘルパーメソッドとして、設定ファイルからHazelcastInstanceを構成するメソッドを用意。

    protected void withHazelcast(String configFilePath, Consumer<HazelcastInstance> consumer) {
        withHazelcast(configFilePath, 1, consumer);
    }

    protected void withHazelcast(String configFilePath, int numInstances, Consumer<HazelcastInstance> consumer) {
        List<HazelcastInstance> hazelcastInstances = IntStream
                .rangeClosed(1, numInstances)
                .mapToObj(i -> {
                    ClasspathXmlConfig config = new ClasspathXmlConfig(configFilePath);
                    return Hazelcast.newHazelcastInstance(config);
                })
                .collect(Collectors.toList());

        hazelcastInstances.forEach(h -> h.getLifecycleService().shutdown());
    }

こちらを使って、設定ファイルを読み込ませてHazelcastクラスタを構成します。

設定ファイルは、Lite Memberとデータを持てるMember用で2つ用意。

Lite Member用。
src/test/resources/hazelcast-litemember.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.6.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <lite-member enabled="true"/>
</hazelcast>

lite-memberタグの、enabled属性をtrueにすれば、それでLite Memberとなります。特定のMapの配下などではありません。

データを持てるMemberの方は、ほぼ設定していませんがわかりやすくTTLくらい設けてみました。
src/test/resources/hazelcast-datamember.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.6.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <map name="default">
        <time-to-live-seconds>3</time-to-live-seconds>
    </map>
</hazelcast>

で、テストコード。クラスタ内のMember数は、Lite Member含めて3にしてみました。

    @Test
    public void preConfigurationedInstances() {
        withHazelcast("hazelcast-datamember.xml", 2, hasDataHazelcast -> {
            withHazelcast("hazelcast-litemember.xml", liteHazelcast -> {
                Map<String, String> map = liteHazelcast.getMap("default");
                map.put("key", "value");

                assertThat(map).containsExactly(MapEntry.entry("key", "value"));

                try {
                    TimeUnit.SECONDS.sleep(5L);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                assertThat(map).isEmpty();

                assertThat(liteHazelcast.getCluster().getMembers())
                        .hasSize(3);
                assertThat(liteHazelcast.getConfig().isLiteMember()).isTrue();
                Set<Partition> partitions = liteHazelcast.getPartitionService().getPartitions();
                assertThat(partitions).hasSize(271);
                assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(2);
            });
        });
    }

Lite Member越しに取得したDistributed Mapにデータを保存したり、TTLが効いていることが確認できます。

その一方で、クラスタの参加数は3ですが、データのオーナーは2であり、Lite Memberは引かれた数となっています。

クラスタ構成時の表示も、2つ分しか見えません。

Members [2] {
	Member [172.17.0.1]:5701
	Member [172.17.0.1]:5702 this
}

まとめ

Hazelcast 3.6で追加された、Lite Memberを試してみました。

クラスタには参加したいけれど、データは持ちたくないMemberとして振る舞いたいというケースで利用するもののようです。

Listenerや特定の処理をキックするフロントになるようなMemberとして利用されたり、Client/Server構成ではできないような機能を利用する場合などに使われたりするのではないでしょうか。

なお、この機能に気付いたきっかけは、Payara 4.1.1.162だったりします。
http://blog.payara.fish/whats-new-in-payara-server-162

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-litemember

Karma × PhantomJs × karma-html2js-preprocessor

以前、karma-html2js-preprocessorを利用して、このようなエントリを書きました。

Karmaでkarma-html2js-preprocessorを使ってHTMLを読み込んでテストする - CLOVER

この時は、以下の組み合わせで環境、コードを用意してテストを書いたのでした。

  • Babel
  • Browserify
  • Karma(ランチャーはChrome
  • karma-html2js-preprocessorでHTMLからのDOMを利用
  • コード自体にはjQueryを利用
  • テストコードは、Mocha+Chai

今回、Karmaのランチャー部をPhantomJsに置き換えてみたいと思います。

KarmaとPhantomJsを組み合わせるには、karma-phantomjs-launcherを利用します。

Karma - Browsers

GitHub - karma-runner/karma-phantomjs-launcher: A Karma plugin. Launcher for PhantomJS.

準備

まずは、必要なnpmモジュールをインストール。

## Babel
$ npm install --save-dev babelify babel-preset-es2015
$ npm install --save babel-polyfill

## Browserify
$ npm install --save-dev browserify watchify

## Mocha, Chai
$ npm install --save-dev mocha chai

## Karma
$ npm install --save-dev karma karma-browserify karma-chai karma-mocha

## Karma & PhantomJs
$ npm install --save-dev karma-phantomjs-launcher
$ npm install --save-dev phantomjs-prebuilt

## Karma Html2Js Preprocessor
$ npm install --save-dev karma-html2js-preprocessor

## jQuery
$ npm install --save jquery

「karma-phantomjs-launcher」と「phantomjs-prebuilt」を入れておくところが、ポイントです。

あとはBabelの設定と

$ echo '{ "presets": ["es2015"] }' > .babelrc

Karmaの設定。

$ ./node_modules/karma/bin/karma init

Karmaの設定で変えたところは、このあたりです。

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['browserify', 'mocha', 'chai'],

    browserify: {
      transform: ['babelify']
    },

    // PhantomJs
    phantomjsLauncher: {
      // Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom)
      exitOnResourceError: true
    },

    // list of files / patterns to load in the browser
    files: [
      'src/**/*.js',
      'test/**/*-test.js',
      'html/**/*.html'
    ],


    // list of files to exclude
    exclude: [
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'src/**/*.js': ['browserify'],
      'test/**/*-test.js': ['browserify'],
      '**/*.html': ['html2js']
    },

phantomjsLauncherの設定を入れています。リソースエラー時に、PhantomJsをexitする設定のようですが…。

あと、今回は自動生成時に選択したのですが、ブラウザをPhantomJsにしています。

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['PhantomJS'],

テストの実行は、「test:karma」となるようにpackage.jsonに定義します。

  "scripts": {
    "test:karma": "karma start",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

コードの用意

コード自体は、元ネタのエントリと同じものを利用します。

テスト対象のコード。
src/click-add.js

import "babel-polyfill";

import $ from "jquery";

export function bindClick() {
    $("#button").bind("click", () => {
        $("#messages").append("<div class='added-message'>Hello Karma!!</div>");
    });
};

テストコード。
test/click-add-test.js

import "chai";
import $ from "jquery";

const should = chai.should();

import { bindClick } from "../src/click-add.js";

describe("click-add test", () => {
    it("click triple from HTML file.", () => {
        document.body.innerHTML = __html__["html/index.html"];

        bindClick();

        let button = document.getElementById("button");
        Array.from(Array(3), (v, k) => k + 1).forEach(i => button.click());

        let messages = document.getElementsByClassName("added-message");
        messages.should.to.have.lengthOf(3);

        let messageTexts = Array.from(messages).map(e => e.textContent);
        messageTexts
            .should
            .deep
            .equal(["Hello Karma!!", "Hello Karma!!", "Hello Karma!!"]);
    });
});

HTMLファイル。
html/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Karma Test Code</title>
</head>
<body>
  <div id="messages"></div>
  <input id="button" type="button" value="クリック!!">
</body>
</html>

実行

それでは、テストを実行してみます。

$ npm run test:karma

> karma-html2js-preprocessor-with-phantomjs@1.0.0 test:karma /path/to/karma-html2js-preprocessor-with-phantomjs
> karma start

17 05 2016 15:55:30.347:INFO [framework.browserify]: registering rebuild (autoWatch=true)
17 05 2016 15:55:33.752:INFO [framework.browserify]: 734545 bytes written (2.66 seconds)
17 05 2016 15:55:33.755:INFO [framework.browserify]: bundle built
17 05 2016 15:55:33.758:WARN [karma]: No captured browser, open http://localhost:9876/
17 05 2016 15:55:33.764:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/
17 05 2016 15:55:33.774:INFO [launcher]: Starting browser PhantomJS
17 05 2016 15:55:34.372:INFO [PhantomJS 2.1.1 (Linux 0.0.0)]: Connected on socket /#Jwo3hVD3Av7kX5_aAAAA with id 57482169
PhantomJS 2.1.1 (Linux 0.0.0): Executed 1 of 1 SUCCESS (0.04 secs / 0.009 secs)

とりあえず、うまくいきましたっと。

ChromeからPhantomJsに変えると、動かすのにちょっとハマったりするかな?と思っていましたが、このくらいの内容ならそれほどでもないみたいでした。
使えそうなところでは、導入したいなぁと思います。