CLOVER🍀

That was when it all began.

Infinispan Server 13をTestcontainersで使う(Infinispan Server Test Driver)

これは、なにをしたくて書いたもの?

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

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 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ですね。

https://github.com/infinispan/infinispan/blob/13.0.8.Final/server/testdriver/core/src/main/java/org/infinispan/server/test/core/InfinispanContainer.java#L28-L30

イメージ自体は、こちらのものが使われます。

Infinispan Server Image

つまり、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というファイルです。

https://github.com/infinispan/infinispan/blob/13.0.8.Final/commons/all/src/main/resources/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をデプロイするようにアーティファクトの指定も
できるみたいです。

https://github.com/infinispan/infinispan/blob/13.0.8.Final/server/testdriver/core/src/main/java/org/infinispan/server/test/core/InfinispanContainer.java#L63-L97

最後は、クラスタリング。コンテナを複数起動すれば、クラスターを構成できます。

    @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 / Examples

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