CLOVER🍀

That was when it all began.

Infinispan Server 12.1で、Hot Rod Client(RemoteCacheManagerAdmin)からCacheを作成する

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

InfinispanのHot Rod Clientには、Cacheの作成、削除などを行う管理APIがあります。

今までも何回か使っていたのですが、よく忘れるので単体でメモしておくことにしました。

RemoteCacheManagerAdminインターフェース

RemoteCacheManagerAdminというインターフェースを使うことで、Infinispan ServerにおけるCacheの作成、削除などが
できます。

RemoteCacheManagerAdmin (Infinispan JavaDoc 12.1.10.Final API)

クラスタ構成のInfinispan Serverであっても、各Nodeに一気にCacheを作れたりするので便利です。
各Nodeそれぞれに対して、XMLファイルを修正しなくてすみますからね。
もっともCLIで操作した場合も同じように各NodeにCacheの定義を反映してくれるのですが、APIで書けた方が
便利な時もあるかな、と。

ちなみに、CLIであっても管理APIであっても、作成したCacheの情報はクラスタに新しいNodeが参加した場合、
そのCacheの情報もコピーされます。この点でも便利です。

ドキュメントとしては、このあたりに記載があります。

Deploying and Configuring Infinispan Servers / Creating Remote Caches with Hot Rod Clients

Hot Rod Java Clients / Creating Remote Caches with Hot Rod Clients

書かれている内容は同じなのですが。

RemoteCacheManagerAdminではなく、RemoteCacheConfigurationBuilder#configurationでCacheの定義を行う方法も
紹介されているのですが、こちらはパスします…。

RemoteCacheConfigurationBuilder (Infinispan JavaDoc 12.1.10.Final API)

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.2 (ea98e05a04480131370aa0c110b8c54cf726c06f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-81-generic", arch: "amd64", family: "unix"

Infinispan Serverは12.1.7.Finalを使い、172.17.0.2〜4の3 Node用意します。

準備

各Nodeには、adminグループに属するユーザーと、applicationグループに属するユーザーをそれぞれ作成しておきます。

$ bin/cli.sh user create -g admin -p password admin-user
$ bin/cli.sh user create -g application -p password app-user

各グループは、ClusterRoleMapperを意識しています。

Deploying and Configuring Infinispan Servers / User Roles and Permissions

Cacheの定義などは行いません。

Maven依存関係などはこちら。

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

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

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.7.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.20.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

今回のお題には、最低限infinispan-client-hotrodが必要です。infinispan-coreはなくてもRemoteCacheManagerAdmin
使うことはできるのですが、あった方がCacheの定義をするには便利かなと思います。理由は後述します。

動作確認はテストコードで行うので、テスト用のライブラリも追加しておきます。

テストコードの雛形

テストコードの雛形は、こちら。

src/test/java/org/littlewings/remote/admin/CreateCacheTest.java

package org.littlewings.remote.admin;

import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

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

public class CreateCacheTest {

    private static String createUri(String userName, String password) {
        return String.format("hotrod://%s:%s@172.17.0.2:11222,172.17.0.3:11222,172.17.0.4:11222", userName, password);
    }

    // ここに、テストコードを書く
}

createUriというメソッドは、接続するユーザー名とパスワードを使って、Infinispan Serverへ接続するためのURI
作成するメソッドです。こちらを接続情報とします。

RemoteCacheManagerAdminを使ってCacheを作成してみる

では、RemoteCacheManagerAdminを使ってCacheを作成してみます。

使用例はこちら。

    @Test
    public void createCacheByAdminUser() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            RemoteCache<String, String> cache = admin.createCache("distCache", cacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));
            assertThat(cache.size()).isEqualTo(100);

            admin.removeCache("distCache");
        }
    }

RemoteCacheManagerAdminは、RemoteCacheManager#administrationで取得できます。
なお、接続にはadminグループに属しているユーザーを使用しています。

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

Cacheの作成はRemoteCacheManagerAdmin#createCacheで行えばよいのですが

            RemoteCache<String, String> cache = admin.createCache("distCache", cacheConfiguration);

この時に作成するCacheの定義情報が必要です。

Cacheの定義情報はStringXMLStringConfigurationから作成でき、依存関係もinfinispan-client-hotrodだけで済みますが
Cacheの定義を文字列で用意することになり、まあ面倒です。この作り方は、最後に載せたいと思います。

もうひとつは、Embedded CacheでのConfigurationを使う方法です。こちらを使うとAPIでCache定義を組み立てられるの
ですが、infinispan-coreが必要になります。依存関係にinfinispan-coreを追加したのはこれが理由ですね。

こんな感じでCacheの定義情報を作成します。infinispan-client-hotrodの範囲外であることがわかるようにFQCN
クラス名を書いています。

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

Cacheの種類としては、Distributed Cacheとしました。

これでクラスタに参加する各NodeにCacheが作成され、すぐに使うことができます。

            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));
            assertThat(cache.size()).isEqualTo(100);

RemoteCacheManagerAdminを使うと、Cacheの削除も可能です。

            admin.removeCache("distCache");

なお、Cacheの作成にはADMIN権限が必要です。このため、applicationグループに属したユーザーでは
RemoteCacheManagerAdmin#createCacheの呼び出しに失敗します。

    @Test
    public void createCacheByApplicationUser() {
        String uri = createUri("app-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThatThrownBy(() -> admin.createCache("distCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheException: java.lang.SecurityException: ISPN000287: Unauthorized access: subject 'Subject with principal(s): [app-user, RolePrincipal{name='application'},")
                    .hasMessageContaining("' lacks 'ADMIN' permission")
                    .isInstanceOf(HotRodClientException.class);
        }
    }

ClusterRoleMapperADMIN権限が使えるのは、ALLが割り当てられているadminグループしかありません。

RemoteCacheManagerAdmin#getOrCreateCacheを使う

RemoteCacheManagerAdmin#createCacheでCacheの作成が可能なことはわかりましたが、作成対象のCacheがすでに
存在している場合は例外がスローされます。

    @Test
    public void recreateCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThat(admin.createCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            assertThatThrownBy(() -> admin.createCache("distCache", cacheConfiguration))
                    .hasMessage("org.infinispan.commons.CacheConfigurationException: ISPN000507: Cache distCache already exists")
                    .isInstanceOf(HotRodClientException.class);

            admin.removeCache("distCache");
        }
    }

これを避ける場合は、RemoteCacheManagerAdmin#getOrCreateCacheを使えばOKです。

    @Test
    public void createOrGetCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            assertThat(admin.createCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);
            assertThat(admin.getOrCreateCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            admin.removeCache("distCache");

            assertThat(admin.getOrCreateCache("distCache", cacheConfiguration))
                    .isNotNull()
                    .isInstanceOf(RemoteCache.class);

            admin.removeCache("distCache");
        }
    }

対象のCacheがなければ作成し、すでに存在していればそれを使うという動作になります。

最初からこれを使ったらいいのでは、という話もありますね。

複数のCacheを作成してみる

ここまでDistributed Cacheばかり作っていましたが、さらにReplicated Cacheも作ってみましょう。

    @Test
    public void createVariousCache() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration distCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            org.infinispan.configuration.cache.Configuration replCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.REPL_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();


            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", distCacheConfiguration);
            RemoteCache<String, String> replCache = admin.getOrCreateCache("replCache", replCacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> distCache.put("key" + i, "value" + i));
            assertThat(distCache.size()).isEqualTo(100);

            IntStream.rangeClosed(1, 100).forEach(i -> replCache.put("key" + i, "value" + i));
            assertThat(replCache.size()).isEqualTo(100);

            admin.removeCache("distCache");
            admin.removeCache("replCache");
        }
    }

まあ、ちょっとしたオマケ的な感じですが…。

で、ここまでCacheを作成してきましたが、Infinispan Server側にどのように存在するかは特に書いてきませんでした。

Cacheを作成しても、infinispan.xmlそのものは変わりません。

server/conf/infinispan.xml

   <cache-container name="default" statistics="true">
      <transport cluster="${infinispan.cluster.name:cluster}" stack="${infinispan.cluster.stack:tcp}" node-name="${infinispan.node.name:}"/>
      <security>
         <authorization/>
      </security>
   </cache-container>

動的に作成したCacheは、server/data/caches.xmlファイルに記載されます。

server/data/caches.xml

<?xml version="1.0"?>
<infinispan xmlns="urn:infinispan:config:12.1">
    <cache-container>
        <replicated-cache mode="SYNC" remote-timeout="17500" name="replCache" statistics="true">
            <encoding>
                <key media-type="application/x-protostream"/>
                <value media-type="application/x-protostream"/>
            </encoding>
            <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
            <state-transfer timeout="60000"/>
        </replicated-cache>
        <distributed-cache mode="SYNC" remote-timeout="17500" name="distCache" statistics="true">
            <encoding>
                <key media-type="application/x-protostream"/>
                <value media-type="application/x-protostream"/>
            </encoding>
            <locking concurrency-level="1000" acquire-timeout="15000" striping="false"/>
            <state-transfer timeout="60000"/>
        </distributed-cache>
    </cache-container>
</infinispan>

ConfigurationBuilderでCacheの定義を行った時はほぼ最低限の設定しか行っていなかったのに、いろいろと
デフォルト値が埋め込まれていますね。

また、最初にも書きましたが、クラスタに新しいNodeが参加した場合は、このファイルも新しいNodeに展開されます。

文字列でCache定義を行う

最後に、Cacheの定義を文字列で作成します。ドキュメントに記載されていたのは、この方法ですね。

    @Test
    public void createCacheByString() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            String cacheConfigurationAsString =
                    "        <distributed-cache name=\"distCache\">\n" +
                            "            <encoding>\n" +
                            "                <key media-type=\"application/x-protostream\"/>\n" +
                            "                <value media-type=\"application/x-protostream\"/>\n" +
                            "            </encoding>\n" +
                            "        </distributed-cache>";

            org.infinispan.commons.configuration.XMLStringConfiguration cacheConfiguration =
                    new org.infinispan.commons.configuration.XMLStringConfiguration(cacheConfigurationAsString);

            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", cacheConfiguration);

            IntStream.rangeClosed(1, 100).forEach(i -> distCache.put("key" + i, "value" + i));
            assertThat(distCache.size()).isEqualTo(100);

            admin.removeCache("distCache");

            assertThatThrownBy(() -> admin.getOrCreateCache("distributedCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheConfigurationException: ISPN005031: The supplied configuration for cache 'distributedCache' is missing a named configuration for it:")
                    .isInstanceOf(HotRodClientException.class);
        }
    }

ポイントはこちらで、Cache定義の断片をXMLStringConfigurationとして作成します。

            String cacheConfigurationAsString =
                    "        <distributed-cache name=\"distCache\">\n" +
                            "            <encoding>\n" +
                            "                <key media-type=\"application/x-protostream\"/>\n" +
                            "                <value media-type=\"application/x-protostream\"/>\n" +
                            "            </encoding>\n" +
                            "        </distributed-cache>";

            org.infinispan.commons.configuration.XMLStringConfiguration cacheConfiguration =
                    new org.infinispan.commons.configuration.XMLStringConfiguration(cacheConfigurationAsString);

これをRemoteCacheManagerAdmin#createCacheRemoteCacheManagerAdmin#getOrCreateCacheに渡せば
OKです。

            RemoteCache<String, String> distCache = admin.getOrCreateCache("distCache", cacheConfiguration);

この方法であればinfinispan-client-hotrodの依存関係に含まれる、infinispan-commonsがあれば依存関係としては
十分です。つまり、Hot Rod Clientのみで実行することができます。

ちなみに、XML定義のCache名と作成するCacheの名前が異なる場合は、例外がスローされるので注意しましょう。。

            assertThatThrownBy(() -> admin.getOrCreateCache("distributedCache", cacheConfiguration))
                    .hasMessageContaining("org.infinispan.commons.CacheConfigurationException: ISPN005031: The supplied configuration for cache 'distributedCache' is missing a named configuration for it:")
                    .isInstanceOf(HotRodClientException.class);

これでひととおり確認できた感じですね。

こちらのチュートリアルを見ていると、Configuration Templateも定義できそうですが、今回はパスします。

https://github.com/infinispan/infinispan-simple-tutorials/tree/main/infinispan-remote/cache-admin-api

Embedded CacheのConfigurationは、どう使われているの?

オマケ的にですが。

こちらがRemoteCacheManagerAdminインターフェースの実装です。

https://github.com/infinispan/infinispan/blob/12.1.7.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/RemoteCacheManagerAdminImpl.java

Embedded CacheのConfigurationを渡した時にはどうなるのかな?と思ったのですが、結局文字列に変換されているので
最終的にはXML定義をInfinispan Serverに送ってCacheを作成するみたいですね。

https://github.com/infinispan/infinispan/blob/12.1.7.Final/client/hotrod-client/src/main/java/org/infinispan/client/hotrod/impl/RemoteCacheManagerAdminImpl.java#L61

まとめ

InfinispanのHot Rod ClientのRemoteCacheManagerAdminを使って、Infinispan Server上にCacheを動的に作成して
みました。

時々使っているのですが、本当によく忘れるのでいい機会かな、と。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-create-cache