これは、なにをしたくて書いたもの?
Infinispan 13のドキュメントに、Testcontainersについて書かれている箇所が増えていることに気づきまして。
Using Hot Rod Java clients / JUnit testing
Using Hot Rod Java clients / Testing Infinispan with test containers
ちょっと試しておこうかな、と。
Infinispan ServerとTestcontainers
Testcontainersは、JUnitテスト内でDockerコンテナを起動するライブラリです。テスト内でデータベースなどを動かすのに使います。
Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.
Testcontainersを使ってInfinispan Serverを実行できるようにしているのが、以下に書かれている箇所の話ですね。
Using Hot Rod Java clients / Testing Infinispan with test containers
Testcontainers自体はGenericContainer
を使うことで汎用的にDockerコンテナを起動できるのですが、Infinispanが提供するクラスを
使うことでInfinispan Serverのカスタマイズがしやすくなったり、RemoteCacheManager
を簡単に取得できるようになります。
Infinispan 13.0.0.Finalで追加されたものになります。
Infinispan Server Test Driver
Infinispanの提供するTestcontainers向けのクラスは、Infinispan Server Test Driverの一部になります。
https://github.com/infinispan/infinispan/tree/13.0.8.Final/server/testdriver
README.md
を読むと、以下の形態でInfinispan Serverを実行できるようです。
- Embedded … テストと同じJavaVMで実行
- Container … コンテナで実行
- Fork … 別プロセスで実行
Testcontainersの機能は、coreに含まれます。
infinispan/server/testdriver/core at 13.0.8.Final · infinispan/infinispan · GitHub
また、JUnit 4/5向けのモジュールも提供されています。
https://github.com/infinispan/infinispan/tree/13.0.8.Final/server/testdriver/junit4
https://github.com/infinispan/infinispan/tree/13.0.8.Final/server/testdriver/junit5
このあたりは、そのうち使ってみるかもしれません…。
Infinispan Arquillian Containerがあるのも知ってはいましたが、今はこの用途も含めてInfinispan Server Test Driverを使うのが良いのでしょう。
GitHub - infinispan/infinispan-arquillian-container: An Arquillian container for Infinispan
とりあえず、説明はこれくらいにして使っていってみましょう。テストは、JUnit 5を使って書きます。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.2 2022-01-18 OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04) OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-107-generic", arch: "amd64", family: "unix"
Docker。
$ docker version Client: Docker Engine - Community Version: 20.10.14 API version: 1.41 Go version: go1.16.15 Git commit: a224086 Built: Thu Mar 24 01:48:02 2022 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.14 API version: 1.41 (minimum version 1.12) Go version: go1.16.15 Git commit: 87a90dc Built: Thu Mar 24 01:45:53 2022 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.5.11 GitCommit: 3df54a852345ae127d1fa3092b95168e4a88e2f8 runc: Version: 1.0.3 GitCommit: v1.0.3-0-gf46b6ba docker-init: Version: 0.19.0 GitCommit: de40ad0
準備
Maven依存関係等は、こちら。
<dependencyManagement> <dependencies> <dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-bom</artifactId> <version>13.0.8.Final</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-client-hotrod</artifactId> </dependency> <dependency> <groupId>org.infinispan.protostream</groupId> <artifactId>protostream-processor</artifactId> <optional>true</optional> </dependency> <!-- <dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-core</artifactId> <scope>test</scope> </dependency> --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.22.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-server-testdriver-core</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.infinispan</groupId> <artifactId>infinispan-server-runtime</artifactId> </exclusion> <exclusion> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build>
ドキュメントに従うと、InfinispanのTestcontainers向けのクラスを使用するにはinfinispan-server-testdriver-core
があればよいことに
なります。
Using Hot Rod Java clients / Adding the test container to your project dependencies
JUnit 5向けのモジュールは必要ありません。
ただ、Infinispan ServerをEmbeddedで利用することもできるため、infinispan-server-testdriver-core
にはInfinispan Serverのモジュールが
まるごとついてきます。これはこれで必要以上に多いな、と思ったので、今回はexclusionしました。
<dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-server-testdriver-core</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.infinispan</groupId> <artifactId>infinispan-server-runtime</artifactId> </exclusion> <exclusion> <groupId>org.jboss.shrinkwrap.resolver</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency>
あとは、なんとなくエンティティをProtocol Buffersでマーシャリングできるようにするためにprotostream-processor
を追加しています。
<dependency> <groupId>org.infinispan.protostream</groupId> <artifactId>protostream-processor</artifactId> <optional>true</optional> </dependency>
Cacheはプログラム内で作成しますが、今回はデフォルトのテンプレートで作成するものとします。
もっと凝りたい場合は、以下をコメントアウトして
<!-- <dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-core</artifactId> <scope>test</scope> </dependency> -->
RemoteCacheManagerAdmin
と組み合わせてCacheを作成するとよいでしょう。
Infinispan Server 12.1で、Hot Rod Client(RemoteCacheManagerAdmin)からCacheを作成する - CLOVER🍀
エンティティの作成
エンティティは、書籍とします。
src/main/java/org/littlewings/infinispan/remote/testcontainers/Book.java
package org.littlewings.infinispan.remote.testcontainers; import org.infinispan.protostream.annotations.ProtoFactory; import org.infinispan.protostream.annotations.ProtoField; public class Book { @ProtoField(number = 1) String isbn; @ProtoField(number = 2) String title; @ProtoField(number = 3, defaultValue = "0") int price; @ProtoFactory public static Book create(String isbn, String title, int price) { Book book = new Book(); book.isbn = isbn; book.title = title; book.price = price; return book; } public String getIsbn() { return isbn; } public String getTitle() { return title; } public int getPrice() { return price; } }
SerializationContextInitializer
インターフェースの拡張。
src/main/java/org/littlewings/infinispan/remote/testcontainers/EntitiesInitializer.java
package org.littlewings.infinispan.remote.testcontainers; import org.infinispan.protostream.SerializationContextInitializer; import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder; @AutoProtoSchemaBuilder( includeClasses = {Book.class}, schemaFileName = "entities.proto", schemaFilePath = "proto", schemaPackageName = "org.littlewings.infinispan.remote.testcontainers" ) public interface EntitiesInitializer extends SerializationContextInitializer { }
では、こちらを使ってテストを書いていきます。
テストを書く
テストコードの雛形は、こちら。
src/test/java/org/littlewings/infinispan/remote/testcontainers/InfinispanServerContainerTest.java
package org.littlewings.infinispan.remote.testcontainers; import java.util.List; import java.util.stream.IntStream; import org.infinispan.client.hotrod.DefaultTemplate; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCacheManager; import org.infinispan.client.hotrod.RemoteCacheManagerAdmin; import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; import org.infinispan.commons.util.Version; import org.infinispan.server.test.core.InfinispanContainer; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class InfinispanServerContainerTest { // ここに、テストを書く! }
まずは、ドキュメントに習って使ってみましょう。
Using Hot Rod Java clients / Starting the Infinispan test container
こんな感じですね。
@Test public void gettingStarted() { try (InfinispanContainer container = new InfinispanContainer()) { container.start(); try (RemoteCacheManager manager = container.getRemoteCacheManager(new ConfigurationBuilder().addContextInitializer(new EntitiesInitializerImpl()))) { RemoteCacheManagerAdmin admin = manager.administration(); RemoteCache<String, Book> cache = admin.createCache("distCache", DefaultTemplate.DIST_SYNC); Book book = Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト] (impress top gear)", 2860); cache.put(book.getIsbn(), book); Book b = cache.get(book.getIsbn()); assertThat(b.getTitle()).isEqualTo("新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト] (impress top gear)"); assertThat(b.getPrice()).isEqualTo(2860); } } }
InfinispanContainer
クラスのインスタンスを作成し、InfinispanContainer#start
とすればOKです。
try (InfinispanContainer container = new InfinispanContainer()) { container.start();
InfinispanContainer
クラスからは、RemoteCacheManager
クラスのインスタンスを取得することができます。
try (RemoteCacheManager manager = container.getRemoteCacheManager(new ConfigurationBuilder().addContextInitializer(new EntitiesInitializerImpl()))) {
InfinispanContainer#getRemoteCacheManager
には引数のあり、なしがあり、どちらにしても認証先や接続情報はセットアップされた状態で
RemoteCacheManager
クラスのインスタンスを取得できます。
今回のようにSerializationContextInitializer
インターフェースの実装を指定したいといった理由などでRemoteCacheManager
クラスを
カスタマイズしたい場合は、ConfigurationBuilder
クラスのインスタンスを渡せばOKです。
このメソッド内で、認証情報なども設定してくれます。
ところで、以下の指定でどのバージョンのInfinispan Serverが利用されるのか、というところですが。
try (InfinispanContainer container = new InfinispanContainer()) {
これはメジャーバージョン+マイナーバージョンの指定になります。今回でいうと、13.0
ですね。
イメージ自体は、こちらのものが使われます。
つまり、quay.io/infinispan/server:13.0
という指定をしていることになります。
使用しているHot Rod Clientなどと同じバージョンを指定したい場合は、以下のように書くとよいでしょう。
try (InfinispanContainer container = new InfinispanContainer(InfinispanContainer.IMAGE_BASENAME + ":" + Version.getVersion())) {
今回はこれでquay.io/infinispan/server:13.0.8.Final
を指定していることになります。
Version#getVersion
の値は、以下のプロパティファイルのinfinispan.version
の値を指しています。
infinispan.version=13.0.8.Final infinispan.core.schema.version=13.0 infinispan.codename=Triskaidekaphobia infinispan.brand.name=Infinispan infinispan.brand.version=13.0.8.Final infinispan.module.slot.prefix=ispn infinispan.module.slot.version=13.0 infinispan.build.commitid=165587b5cbcc16e118134c40a937de679548e924 infinispan.build.branch=13.0.x infinispan.olm.openshift.source=community-operators infinispan.olm.k8s.source=operatorhubio-catalog infinispan.olm.name=infinispan
ちなみに、デフォルトのバージョン指定で参照されているのはinfinispan.core.schema.version
ですね。
確認。
@Test public void version() { assertThat(Version.getVersion()).isEqualTo("13.0.8.Final"); assertThat(Version.getMajorMinor()).isEqualTo("13.0"); }
このファイルは、infinispan-commons
に含まれているMETA-INF/infinispan-version.properties
というファイルです。
先に進みましょう。クレデンシャルはデフォルトでadmin
/ secret
になっており、これは変更することができます。
Using Hot Rod Java clients / Changing credentials
ここまでを合わせると、こんな感じですね。
@Test public void credentialTest() { try (InfinispanContainer container = new InfinispanContainer(InfinispanContainer.IMAGE_BASENAME + ":" + Version.getVersion()) .withUser("ispn-user") .withPassword("ispn-password")) { container.start(); try (RemoteCacheManager manager = container.getRemoteCacheManager(new ConfigurationBuilder().addContextInitializer(new EntitiesInitializerImpl()))) { RemoteCacheManagerAdmin admin = manager.administration(); RemoteCache<String, Book> cache = admin.createCache("distCache", DefaultTemplate.DIST_SYNC); Book book = Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト] (impress top gear)", 2860); cache.put(book.getIsbn(), book); Book b = cache.get(book.getIsbn()); assertThat(b.getTitle()).isEqualTo("新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト] (impress top gear)"); assertThat(b.getPrice()).isEqualTo(2860); } } }
RemoteCacheManager
クラスのインスタンスを取得する際には、カスタマイズしたクレデンシャルを意識する必要はありません。
InfinispanServer
クラス固有のカスタマイズは、クレデンシャルに加え、Server Taskをデプロイするようにアーティファクトの指定も
できるみたいです。
最後は、クラスタリング。コンテナを複数起動すれば、クラスターを構成できます。
@Test public void clustered() { List<InfinispanContainer> containers = IntStream .rangeClosed(1, 3) .mapToObj(i -> new InfinispanContainer(InfinispanContainer.IMAGE_BASENAME + ":" + Version.getVersion())) .toList(); containers.forEach(InfinispanContainer::start); try { InfinispanContainer container = containers.get(0); try (RemoteCacheManager manager = container.getRemoteCacheManager(new ConfigurationBuilder().addContextInitializer(new EntitiesInitializerImpl()))) { RemoteCacheManagerAdmin admin = manager.administration(); RemoteCache<String, Book> cache = admin.createCache("distCache", DefaultTemplate.DIST_SYNC); Book book = Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト] (impress top gear)", 2860); cache.put(book.getIsbn(), book); Book b = cache.get(book.getIsbn()); assertThat(b.getTitle()).isEqualTo("新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト] (impress top gear)"); assertThat(b.getPrice()).isEqualTo(2860); assertThat(manager.getServers()).hasSize(4); } } finally { containers.forEach(InfinispanContainer::close); } }
今回は、3つのInfinispan Serverのコンテナを起動してクラスターを組んでみました。
4月 01, 2022 12:15:20 午前 org.infinispan.client.hotrod.impl.transport.netty.ChannelFactory receiveTopology INFO: ISPN004006: Server sent new topology view (id=9, age=0) containing 3 addresses: [172.17.0.4/<unresolved>:11222, 172.17.0.3/<unresolved>:11222, 172.17.0.5/<unresol ved>:11222] 4月 01, 2022 12:15:20 午前 org.infinispan.client.hotrod.impl.transport.netty.ChannelFactory updateCacheInfo INFO: ISPN004014: New server added(172.17.0.4/<unresolved>:11222), adding to the pool. 4月 01, 2022 12:15:20 午前 org.infinispan.client.hotrod.impl.transport.netty.ChannelFactory updateCacheInfo INFO: ISPN004014: New server added(172.17.0.5/<unresolved>:11222), adding to the pool. 4月 01, 2022 12:15:20 午前 org.infinispan.client.hotrod.impl.transport.netty.ChannelFactory updateCacheInfo INFO: ISPN004014: New server added(172.17.0.3/<unresolved>:11222), adding to the pool. 4月 01, 2022 12:15:20 午前 org.infinispan.client.hotrod.impl.transport.netty.ChannelFactory closeChannelPools INFO: ISPN004016: Server not in cluster anymore(localhost/<unresolved>:49810), removing from the pool. 4月 01, 2022 12:15:21 午前 org.infinispan.client.hotrod.impl.transport.netty.ChannelFactory$ReleaseChannelOperation cancel WARN: ISPN004015: Failed adding new server 172.17.0.4/<unresolved>:11222
ところがログを見るとひとつ不思議なInfinispan Server(?)を認識しているようで、4つになってしまいます。
assertThat(manager.getServers()).hasSize(4);
実際のクラスター内には3つのInfinispan Serverのみしかないんですけどね。
RemoteCacheManager#getServers
の値を見ると、以下のようになっています。
["172.17.0.5:11222", "172.17.0.2:11222", "172.17.0.3:11222", "localhost:49810"]
実は、Infinispan Serverコンテナをひとつしか起動していなくても、やっぱり+1されてしまいます。
どうなっているんでしょうね…。今回は、あまり踏み込まないことにしますが…。
オマケ
今回はテストケースの実行の度にInfinispan Serverコンテナを起動しているので、テストがとても遅くなります。
テスト間でコンテナを共有して起動回数を減らしたい場合は、以下のページを参考にするとよいでしょう。
JUnit 5 Quickstart / Get Testcontainers to run a Redis container during our tests
Jupiter / JUnit 5 / Shared containers
Jupiter / JUnit 5 / Singleton containerss
Manual container lifecycle control / Singleton containers
まとめ
Infinispan Server Test Driverに含まれる、Testcontainers向けのInfinispanContainer
を使ってみました。
久しぶりにTestcontainersを使ったことと、Infinispan Server Test Driverのモジュールの関係性に若干混乱しましたが(最初はJUnit 5向けの
ものを使えばいいと思っていました)、把握できればあとはそれほど困ることはありませんでした。
RemoteCacheManager
が認識しているInfinispan Serverの数だけちょっと微妙でしたが…まあいいでしょう。
今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-testcontainers