CLOVER🍀

That was when it all began.

Infinispan Server 14.0.1.Finalで、Server Taskが呼び出しごとにインスタンス化できたり、ADMIN権限が不要になったりしていた話

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

InfinispanのServer Taskのソースコードを見ていたら、前に踏んでいた問題がいろいろ直っているのに気づきまして。

せっかくなので、確認しておこうかなと。

前に踏んでいた問題

このエントリーを書いた時の話なのですが、以下のようなところに引っかかりました。

  • Server Taskの実行にあたり、クライアントから接続するユーザーにADMIN権限が必要
  • Server Taskの実行モードを全ノード(ALL_NODES)にして、かつMarshallingをProtoStreamにすると実行に失敗する
  • Server Taskの実行モードを全ノード(ALL_NODES)にすると、渡したパラメーターがすべてStringになる

Infinispan Server 12.1でProtoStreamでのMarshallingを使いつつ、Server Taskを実行する - CLOVER🍀

加えて、Server Taskのインスタンスは実はシングルトンであったりするという話がありました。

渡したパラメーターがすべてStringになるという点を除いて、これらの問題は修正されているようです。

以下のissueに対する修正で実行にADMIN権限が不要になり、

[ISPN-14143] Task execution needs ADMIN permission additional to EXEC - Red Hat Issue Tracker

こちらのissueに対する修正で実行モードが全ノード(ALL_NOES)であっても実行に失敗しなくなったようです。

[ISPN-14131] ProtoStream will throw an Exception if a remote-task is executed in TaskExecutionMode.ALL_NODES - Red Hat Issue Tracker

また、実行モードによって権限が異なるというissueの修正の関連で

[ISPN-14144] Task execution (permission) is different for ALL_NODES and ONE_NODE - Red Hat Issue Tracker

どうもServer Taskのインスタンスの生成について指定ができるようになったみたいです。

ISPN-14144 Propagate subject in task execution by tristantarrant · Pull Request #10336 · infinispan/infinispan · GitHub

インスタンス生成については、ServerTaskインターフェースの親インターフェースである、Taskを確認します。

Task (Infinispan JavaDoc 14.0.1.Final API)

Task#getInstantiationModeで返す値で制御できるようになったみたいです。

Task#getInstantiationMode)

戻り値に使用するのはTaskInstantiationMode列挙型で、デフォルトはSHAREDでシングルトンとなりServer Taskのインスタンスは1度だけ
作成され、リクエストを跨いで使い回されます。ISOLATEDはServer Taskの呼び出しごとにインスタンスが作成されます。

TaskInstantiationMode (Infinispan JavaDoc 14.0.1.Final API)

Infinispan Serverのガイドにも追記がありました。

ServerTask#setTaskContextの説明を見ると、以下のように書かれています。

In most cases, implementations store this information locally and use it when tasks are actually executed. When using SHARED instantiation mode, the task should use a ThreadLocal to store the TaskContext for concurrent invocations.

Guide to Infinispan Server / Running scripts and tasks on Infinispan Server / Adding tasks to Infinispan Server deployments / Infinispan Server tasks

TaskInstantiationModeがSHAREDの時は、ここで渡された値をThreadLocalで保持する必要があると書かれています。

合わせて、サンプルコードもThreadLocalを使うように修正されています。

package example;

import org.infinispan.tasks.ServerTask;
import org.infinispan.tasks.TaskContext;

public class HelloTask implements ServerTask<String> {

   private static final ThreadLocal<TaskContext> taskContext = new ThreadLocal<>();

   @Override
   public void setTaskContext(TaskContext ctx) {
      taskContext.set(ctx);
   }

   @Override
   public String call() throws Exception {
      TaskContext ctx = taskContext.get();
      String name = (String) ctx.getParameters().get().get("name");
      return "Hello " + name;
   }

   @Override
   public String getName() {
      return "hello-task";
   }

}

Server Taskのインスタンスは前々からシングルトンだったので、こうする必要があるように思っていましたがドキュメントが修正されたので
答え合わせができた気になりました。

せっかく修正されたので、今回は権限とServer Taskのインスタンス化について確認してみたいと思います。

なお、Server Taskの実行モードを全ノード(ALL_NODES)にすると、渡したパラメーターがすべてStringになるという点に関しては、
変わらないよう(変わりようがない?)気がしますね。

https://github.com/infinispan/infinispan/blob/14.0.1.Final/tasks/api/src/main/java/org/infinispan/tasks/TaskContext.java#L198-L211

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.4, 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-131-generic", arch: "amd64", family: "unix"

Infinispan Serverは、172.18.0.2〜172.18.0.4の3つのノードで動作し、クラスターを構成しているものとします。

$ java --version
openjdk 17.0.4.1 2022-08-12
OpenJDK Runtime Environment Temurin-17.0.4.1+1 (build 17.0.4.1+1)
OpenJDK 64-Bit Server VM Temurin-17.0.4.1+1 (build 17.0.4.1+1, mixed mode, sharing)


$ bin/server.sh --version

Infinispan Server 14.0.1.Final (Flying Saucer)
Copyright (C) Red Hat Inc. and/or its affiliates and other contributors
License Apache License, v. 2.0. http://www.apache.org/licenses/LICENSE-2.0

起動コマンドはこちら。

$ bin/server.sh \
    -b 0.0.0.0 \
    -Djgroups.tcp.address=`hostname -i`

お題

今回のお題は、以下のようにします。

  • Mavenプロジェクトをひとつ作成
  • src/main/java側でInfinispan ServerのServer Taskを作成
    • 処理内容は同じで、TaskInstantiationModeの指定をデフォルト(SHARED)とISOLATEDの2つのServer Taskを作成
    • インスタンス化の回数を確認するために、呼び出しごとにカウントアップする処理を入れておく
    • 処理対象ノードは、すべてのノードとしてMarshallingの確認を行う
  • 動作確認はテストコードで行う

準備

Maven依存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-tasks-api</artifactId>
            <version>14.0.1.Final</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>14.0.1.Final</version>
            <scope>test</scope>
        </dependency>
        <!-- RemoteCacheManagerAdminで使用、infinispan-tasks-apiに含まれている -->
        <!--
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>14.0.1.Final</version>
            <scope>test</scope>
        </dependency>
        -->

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.23.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

Infinispan Serverの各Nodeには、管理ユーザーおよびアプリケーションユーザーを作成しておきます。

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

Server Taskを作成する

では、まずはServer Taskを作成していきます。

最初はTaskInstantiationMode#SHAREDのServer Task。このServer Taskは、処理を実行する際に1度だけインスタンス化され、
あとはリクエストをまたがってインスタンスが再利用されます。

src/main/java/org/littlewings/infinispan/remote/task/SharedInstanceSummarizeServerTask.java

package org.littlewings.infinispan.remote.task;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.infinispan.Cache;
import org.infinispan.stream.CacheCollectors;
import org.infinispan.tasks.ServerTask;
import org.infinispan.tasks.TaskContext;
import org.infinispan.tasks.TaskExecutionMode;

public class SharedInstanceSummarizeServerTask implements ServerTask<String> {
    AtomicInteger callCounter = new AtomicInteger();

    ThreadLocal<TaskContext> threadLocalTaskContext = new ThreadLocal<>();

    @Override
    public void setTaskContext(TaskContext taskContext) {
        this.threadLocalTaskContext.set(taskContext);
    }

    @Override
    public String call() throws Exception {
        TaskContext taskContext = threadLocalTaskContext.get();

        try {
            Map<String, Object> parameters = taskContext.getParameters().orElse(Collections.emptyMap());

            int called = callCounter.incrementAndGet();

            @SuppressWarnings("unchecked")
            Cache<String, Integer> cache = (Cache<String, Integer>) taskContext.getCache().get();
            int sum = cache.values().stream().collect(CacheCollectors.serializableCollector(() -> Collectors.summingInt(i -> i)));

            return String.format("%s, shared instance server task call count = %d, sum result = %d", parameters.get("message"), called, sum);
        } finally {
            threadLocalTaskContext.remove();
        }
    }

    @Override
    public String getName() {
        return SharedInstanceSummarizeServerTask.class.getSimpleName();
    }

    @Override
    public TaskExecutionMode getExecutionMode() {
        return TaskExecutionMode.ALL_NODES;
    }

    /* デフォルトで以下と同じ
    @Override
    public TaskInstantiationMode getInstantiationMode() {
        return TaskInstantiationMode.SHARED;
    }
    */
}

TaskInstantiationModeはServerTask#getInstantiationModeメソッドをオーバーライドして指定する必要がありますが、デフォルトが
TaskInstantiationMode#SHAREDなので今回はそのままいこうと思います。

    /* デフォルトで以下と同じ
    @Override
    public TaskInstantiationMode getInstantiationMode() {
        return TaskInstantiationMode.SHARED;
    }
    */

TaskContextは同時呼び出しされることに備え、ドキュメントに習ってThreadLocalで保持します。

    ThreadLocal<TaskContext> threadLocalTaskContext = new ThreadLocal<>();

    @Override
    public void setTaskContext(TaskContext taskContext) {
        this.threadLocalTaskContext.set(taskContext);
    }

今回ドキュメントに明記されましたが、今までもこうするべきだったんでしょうね。

TaskContextはcallメソッドの最初に取り出して、

    @Override
    public String call() throws Exception {
        TaskContext taskContext = threadLocalTaskContext.get();

最後に削除。

        } finally {
            threadLocalTaskContext.remove();
        }

あとは、インスタンスが1度しか作成されていないことを確認するために、カウンターを用意しておきます。

    AtomicInteger callCounter = new AtomicInteger();

これで、呼び出しごとにメッセージ内の値がカウントアップされていくはずです。

            return String.format("%s, shared instance server task call count = %d, sum result = %d", parameters.get("message"), called, sum);

実行ノードは、全ノードが対象です。

    @Override
    public TaskExecutionMode getExecutionMode() {
        return TaskExecutionMode.ALL_NODES;
    }

なお、文字列の送受信だけではなんなので、Cacheを使ったコードも入れておきました。

            @SuppressWarnings("unchecked")
            Cache<String, Integer> cache = (Cache<String, Integer>) taskContext.getCache().get();
            int sum = cache.values().stream().collect(CacheCollectors.serializableCollector(() -> Collectors.summingInt(i -> i)));

続いては、呼び出しの都度インスタンス化を行うServer Task。TaskInstantiationMode#ISOLATEDを指定するバージョンですね。

src/main/java/org/littlewings/infinispan/remote/task/IsolatedInstanceSummarizeServerTask.java

package org.littlewings.infinispan.remote.task;

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.infinispan.Cache;
import org.infinispan.stream.CacheCollectors;
import org.infinispan.tasks.ServerTask;
import org.infinispan.tasks.TaskContext;
import org.infinispan.tasks.TaskExecutionMode;
import org.infinispan.tasks.TaskInstantiationMode;

public class IsolatedInstanceSummarizeServerTask implements ServerTask<String> {
    AtomicInteger callCounter = new AtomicInteger();

    TaskContext taskContext;

    @Override
    public void setTaskContext(TaskContext taskContext) {
        this.taskContext = taskContext;
    }

    @Override
    public String call() throws Exception {
        Map<String, Object> parameters = taskContext.getParameters().orElse(Collections.emptyMap());

        int called = callCounter.incrementAndGet();

        @SuppressWarnings("unchecked")
        Cache<String, Integer> cache = (Cache<String, Integer>) taskContext.getCache().get();
        int sum = cache.values().stream().collect(CacheCollectors.serializableCollector(() -> Collectors.summingInt(i -> i)));

        return String.format("%s, isolated instance server task call count = %d, sum result = %d", parameters.get("message"), called, sum);
    }

    @Override
    public String getName() {
        return IsolatedInstanceSummarizeServerTask.class.getSimpleName();
    }

    @Override
    public TaskExecutionMode getExecutionMode() {
        return TaskExecutionMode.ALL_NODES;
    }

    @Override
    public TaskInstantiationMode getInstantiationMode() {
        return TaskInstantiationMode.ISOLATED;
    }
}

先ほどとの違いは、まずはServerTask#getInstantiationModeメソッドでTaskInstantiationMode#ISOLATEDを返すようにしていることですね。

    @Override
    public TaskInstantiationMode getInstantiationMode() {
        return TaskInstantiationMode.ISOLATED;
    }

これで、呼び出し都度このServer Taskがインスタンス化されることになります。

このため、TaskContextをThreadLocalで保持する必要はなくなります。

    TaskContext taskContext;

    @Override
    public void setTaskContext(TaskContext taskContext) {
        this.taskContext = taskContext;
    }

最後に、Server TaskはService Loaderの仕組みでロードされるため、META-INF/services/org.infinispan.tasks.ServerTaskというファイルに
今回作成したクラスのFQCNを書いておきます。

src/main/resources/META-INF/services/org.infinispan.tasks.ServerTask

org.littlewings.infinispan.remote.task.SharedInstanceSummarizeServerTask
org.littlewings.infinispan.remote.task.IsolatedInstanceSummarizeServerTask

あとはパッケージングして

$ mvn package -DskipTests=true

Infinispan Serverにデプロイ。Server TaskのJARファイルを、server/libにコピーします。

$ cp /path/to/target/remote-task-instantiation-0.0.1-SNAPSHOT.jar server/lib

JARファイルを配置したら、Infinispan Serverを再起動します。

起動中のログで、作成したServer Taskがロードされているのを確認しましょう。

2022-10-25 15:05:34,561 INFO  (main) [org.infinispan.SERVER] ISPN080027: Loaded extension 'org.littlewings.infinispan.remote.task.SharedInstanceSummarizeServerTask'
2022-10-25 15:05:34,561 INFO  (main) [org.infinispan.SERVER] ISPN080027: Loaded extension 'org.littlewings.infinispan.remote.task.IsolatedInstanceSummarizeServerTask'

同じことを、全ノードに対して行ったらデプロイ完了です。

確認する

確認は、テストコードで行います。

作成したテストコードはこちら。

src/test/java/org/littlewings/infinispan/remote/task/RemoteTaskInstantiationTest.java

package org.littlewings.infinispan.remote.task;

import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.IntStream;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.configuration.cache.CacheMode;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

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

public class RemoteTaskInstantiationTest {
    @BeforeAll
    static void createCache() {
        Configuration configuration =
                new ConfigurationBuilder()
                        .uri("hotrod://ispn-admin:password@172.18.0.2:11222,172.18.0.3:11222,172.18.0.4:11222")
                        .build();

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

            admin.removeCache("distCache");

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering().cacheMode(CacheMode.DIST_SYNC)
                            .security().authorization().enable()
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();
            admin.getOrCreateCache("distCache", cacheConfiguration);
        }
    }

    <K, V> void withRemoteCache(String cacheName, Consumer<RemoteCache<K, V>> consumer) {
        Configuration configuration =
                new ConfigurationBuilder()
                        .uri("hotrod://ispn-user:password@172.18.0.2:11222,172.18.0.3:11222,172.18.0.4:11222")
                        .build();


        try (RemoteCacheManager manager = new RemoteCacheManager(configuration)) {
            RemoteCache<K, V> cache = manager.getCache(cacheName);

            consumer.accept(cache);
        }
    }

    @Test
    public void instanceSharedTasks() {
        this.<String, Integer>withRemoteCache("distCache", cache -> {
            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, i));

            assertThat(cache).hasSize(100);

            List<String> results1 =
                    cache.execute("SharedInstanceSummarizeServerTask", Map.of("message", "hello"));

            assertThat(results1)
                    .hasSize(3)
                    .containsOnly(
                            "hello, shared instance server task call count = 1, sum result = 5050",
                            "hello, shared instance server task call count = 1, sum result = 5050",
                            "hello, shared instance server task call count = 1, sum result = 5050"
                    );

            List<String> results2 =
                    cache.execute("SharedInstanceSummarizeServerTask", Map.of("message", "world"));

            assertThat(results2)
                    .hasSize(3)
                    .containsOnly(
                            "world, shared instance server task call count = 2, sum result = 5050",
                            "world, shared instance server task call count = 2, sum result = 5050",
                            "world, shared instance server task call count = 2, sum result = 5050"
                    );

            List<String> results3 =
                    cache.execute("SharedInstanceSummarizeServerTask", Map.of("message", "yeah"));

            assertThat(results3)
                    .hasSize(3)
                    .containsOnly(
                            "yeah, shared instance server task call count = 3, sum result = 5050",
                            "yeah, shared instance server task call count = 3, sum result = 5050",
                            "yeah, shared instance server task call count = 3, sum result = 5050"
                    );

            cache.clear();
        });
    }

    @Test
    public void instanceIsolatedTasks() {
        this.<String, Integer>withRemoteCache("distCache", cache -> {
            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, i));

            assertThat(cache).hasSize(100);

            List<String> results1 =
                    cache.execute("IsolatedInstanceSummarizeServerTask", Map.of("message", "hello"));

            assertThat(results1)
                    .hasSize(3)
                    .containsOnly(
                            "hello, isolated instance server task call count = 1, sum result = 5050",
                            "hello, isolated instance server task call count = 1, sum result = 5050",
                            "hello, isolated instance server task call count = 1, sum result = 5050"
                    );

            List<String> results2 =
                    cache.execute("IsolatedInstanceSummarizeServerTask", Map.of("message", "world"));

            assertThat(results2)
                    .hasSize(3)
                    .containsOnly(
                            "world, isolated instance server task call count = 1, sum result = 5050",
                            "world, isolated instance server task call count = 1, sum result = 5050",
                            "world, isolated instance server task call count = 1, sum result = 5050"
                    );

            List<String> results3 =
                    cache.execute("IsolatedInstanceSummarizeServerTask", Map.of("message", "yeah"));

            assertThat(results3)
                    .hasSize(3)
                    .containsOnly(
                            "yeah, isolated instance server task call count = 1, sum result = 5050",
                            "yeah, isolated instance server task call count = 1, sum result = 5050",
                            "yeah, isolated instance server task call count = 1, sum result = 5050"
                    );

            cache.clear();
        });
    }
}

テストコードの内容を説明していきます。

こちらは、テストの最初にキャッシュの作成を行うようにしています。こちらの接続で使用しているユーザーは、管理ユーザーです。

    @BeforeAll
    static void createCache() {
        Configuration configuration =
                new ConfigurationBuilder()
                        .uri("hotrod://ispn-admin:password@172.18.0.2:11222,172.18.0.3:11222,172.18.0.4:11222")
                        .build();

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

            admin.removeCache("distCache");

            org.infinispan.configuration.cache.Configuration cacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering().cacheMode(CacheMode.DIST_SYNC)
                            .security().authorization().enable()
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();
            admin.getOrCreateCache("distCache", cacheConfiguration);
        }
    }

キャッシュの種類は、なんとなくDistributed Cacheです。

こちらは、テスト内で使用するRemoteCacheを作成するメソッド。接続にはアプリケーション用のユーザーを使用します。

    <K, V> void withRemoteCache(String cacheName, Consumer<RemoteCache<K, V>> consumer) {
        Configuration configuration =
                new ConfigurationBuilder()
                        .uri("hotrod://ispn-user:password@172.18.0.2:11222,172.18.0.3:11222,172.18.0.4:11222")
                        .build();


        try (RemoteCacheManager manager = new RemoteCacheManager(configuration)) {
            RemoteCache<K, V> cache = manager.getCache(cacheName);

            consumer.accept(cache);
        }
    }

あとはテストです。

最初は、TaskInstantiationModeをTaskInstantiationMode#SHAREDにしたServer Taskに対する呼び出し。

    @Test
    public void instanceSharedTasks() {
        this.<String, Integer>withRemoteCache("distCache", cache -> {
            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, i));

            assertThat(cache).hasSize(100);

            List<String> results1 =
                    cache.execute("SharedInstanceSummarizeServerTask", Map.of("message", "hello"));

            assertThat(results1)
                    .hasSize(3)
                    .containsOnly(
                            "hello, shared instance server task call count = 1, sum result = 5050",
                            "hello, shared instance server task call count = 1, sum result = 5050",
                            "hello, shared instance server task call count = 1, sum result = 5050"
                    );

            List<String> results2 =
                    cache.execute("SharedInstanceSummarizeServerTask", Map.of("message", "world"));

            assertThat(results2)
                    .hasSize(3)
                    .containsOnly(
                            "world, shared instance server task call count = 2, sum result = 5050",
                            "world, shared instance server task call count = 2, sum result = 5050",
                            "world, shared instance server task call count = 2, sum result = 5050"
                    );

            List<String> results3 =
                    cache.execute("SharedInstanceSummarizeServerTask", Map.of("message", "yeah"));

            assertThat(results3)
                    .hasSize(3)
                    .containsOnly(
                            "yeah, shared instance server task call count = 3, sum result = 5050",
                            "yeah, shared instance server task call count = 3, sum result = 5050",
                            "yeah, shared instance server task call count = 3, sum result = 5050"
                    );

            cache.clear();
        });
    }

呼び出しごとに、カウントアップされていますね。これは、Server Taskのインスタンスが使い回されているからです。

続いて、TaskInstantiationModeをTaskInstantiationMode#ISOLATEDにしたServer Taskに対する呼び出し。

    @Test
    public void instanceIsolatedTasks() {
        this.<String, Integer>withRemoteCache("distCache", cache -> {
            IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, i));

            assertThat(cache).hasSize(100);

            List<String> results1 =
                    cache.execute("IsolatedInstanceSummarizeServerTask", Map.of("message", "hello"));

            assertThat(results1)
                    .hasSize(3)
                    .containsOnly(
                            "hello, isolated instance server task call count = 1, sum result = 5050",
                            "hello, isolated instance server task call count = 1, sum result = 5050",
                            "hello, isolated instance server task call count = 1, sum result = 5050"
                    );

            List<String> results2 =
                    cache.execute("IsolatedInstanceSummarizeServerTask", Map.of("message", "world"));

            assertThat(results2)
                    .hasSize(3)
                    .containsOnly(
                            "world, isolated instance server task call count = 1, sum result = 5050",
                            "world, isolated instance server task call count = 1, sum result = 5050",
                            "world, isolated instance server task call count = 1, sum result = 5050"
                    );

            List<String> results3 =
                    cache.execute("IsolatedInstanceSummarizeServerTask", Map.of("message", "yeah"));

            assertThat(results3)
                    .hasSize(3)
                    .containsOnly(
                            "yeah, isolated instance server task call count = 1, sum result = 5050",
                            "yeah, isolated instance server task call count = 1, sum result = 5050",
                            "yeah, isolated instance server task call count = 1, sum result = 5050"
                    );

            cache.clear();
        });
    }

こちらは、カウントアップされません。呼び出しごとにServer Taskのインスタンスが作られているからですね。

というわけで、確認しておきたかったことはこれで済みました。

ソースコードを見てみる

今回の内容について、Infinispan側のソースコードを少し見ておきましょう。

まず、Server Taskのインスタンス化の制御についてはこちらです。

   public T run(TaskContext context) throws Exception {
      final ServerTask<T> t;
      if (task.getInstantiationMode() == TaskInstantiationMode.ISOLATED) {
         t = Util.getInstance(task.getClass());
      } else {
         t = task;
      }
      t.setTaskContext(context);
      if (log.isTraceEnabled()) {
         log.tracef("Executing task '%s' in '%s' mode using context %s", getName(), getInstantiationMode(), context);
      }
      return t.call();
   }

https://github.com/infinispan/infinispan/blob/14.0.1.Final/server/runtime/src/main/java/org/infinispan/server/tasks/ServerTaskWrapper.java#L33-L45

Server Taskの実行にあたり、Infinispan Serverに接続するユーザーにADMIN権限が必要とされていた点については、Pull Requestで
ソースコードが大量に変わっていたのであんまり深追いしませんでした…。

https://github.com/infinispan/infinispan/blob/14.0.1.Final/server/runtime/src/main/java/org/infinispan/server/tasks/ServerTaskEngine.java#L93

また、MarshallerをProtoStreamにして実行ノードを全ノードにした場合にMarshallingに失敗する問題は、以下の部分で
Collections#synchronizedListをArrayListにしたことで解消されています。

https://github.com/infinispan/infinispan/blob/14.0.1.Final/server/runtime/src/main/java/org/infinispan/server/tasks/DistributedServerTaskRunner.java#L30

確認は、こんなところで。

まとめ

今回は、Infinispan ServerのServer Taskで、以前にいくつか困ったことがある問題が修正されているのを確認してみました。

だいぶ使いやすくなったのではないかな、と思います。

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

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-task-instantiation

WildFly Bootable JAR作成時に使う、WildFly JAR Maven Pluginのjboss-maven-distオプションの有効/無効を切り替える

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

WildFly Bootable JARを作成するWildFly JAR Maven Pluginの設定に、jboss-maven-distという設定があります。

<plugin-options>
    <jboss-maven-dist/>
</plugin-options>

こちらについて、ちょっと気になったことがあったので調べてみました。

jboss-maven-dist plugin-opsion

jboss-maven-distというのはWildFly JAR Maven Pluginの設定で、WildFly Bootable JARをスリムにするものです。

具体的には以下のように指定するだけで、JBossモジュールのJARファイルを含まないWildFly Bootable JARを作成できます。

<plugin-options>
    <jboss-maven-dist/>
</plugin-options>

WildFly Bootable JAR Documentation / Advanced usages / Provisioning a slim bootable JAR

この効果としては、WildFly Bootable JARのサイズも小さくなり、起動も高速になるという利点があります。

JBossモジュールのJARファイル自体はどこから取得するかというと、ローカルのMavenリポジトリから、ということになります。

これはこれで開発時には便利なのですが、実際の環境で動かす時には必要なJARファイルをすべて含んだものを作成した方がよいでしょう。
となると、ドキュメントを見ていてもjboss-maven-distには設定の切り替えを行うような例がなかったので、jboss-maven-distを
含む・含まないでMavenのプロファイルあたりで切り替えるのが良いのかなと思っていたのですが。

ソースコードを眺めていると、どうもtrue/falseで切り替えられそうな雰囲気がありました。

    private boolean isThinServer() throws ProvisioningException {
        if (!pluginOptions.containsKey(JBOSS_MAVEN_DIST)) {
            return false;
        }
        final String value = pluginOptions.get(JBOSS_MAVEN_DIST);
        return value == null ? true : Boolean.parseBoolean(value);
    }

https://github.com/wildfly-extras/wildfly-jar-maven-plugin/blob/8.0.1.Final/plugin/src/main/java/org/wildfly/plugins/bootablejar/maven/goals/AbstractBuildBootableJarMojo.java#L1545-L1551

plugin-optionsの説明を見ても、その配下で指定できるオプションについては書かれていませんからね。

WildFly Bootable JAR Documentation / Maven Plugin / dev / Parameter Details / pluginOptions

今回、ちょっと試してみることにしました。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.4, 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-131-generic", arch: "amd64", family: "unix"

アプリケーションを作成する

簡単にアプリケーションを作成します。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>wildfly-bootable-jar-switch-jboss-maven-dist</artifactId>
    <version>0.0-1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>8.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-jar-maven-plugin</artifactId>
                <version>8.0.1.Final</version>
                <configuration>
                    <feature-pack-location>wildfly@maven(org.jboss.universe:community-universe)#26.1.2.Final</feature-pack-location>
                    <layers>
                        <layer>jaxrs-server</layer>
                    </layers>
                    <plugin-options>
                        <jboss-maven-dist/>
                    </plugin-options>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

CDI管理Bean。

src/main/java/org/littlewings/wildfly/bootable/MessageService.java

package org.littlewings.wildfly.bootable;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MessageService {
    public String get() {
        return "Hello WildFly Bootable JAR!!";
    }
}

JAX-RSリソースクラス。

src/main/java/org/littlewings/wildfly/bootable/HelloResource.java

package org.littlewings.wildfly.bootable;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("hello")
public class HelloResource {
    @Inject
    MessageService messageService;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String message() {
        return messageService.get();
    }
}

JAX-RSの有効化。

src/main/java/org/littlewings/wildfly/bootable/JaxrsActivator.java

package org.littlewings.wildfly.bootable;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
}

さらっと流しましたが、WildFly JAR Maven Pluginの設定はこんな感じです。

            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-jar-maven-plugin</artifactId>
                <version>8.0.1.Final</version>
                <configuration>
                    <feature-pack-location>wildfly@maven(org.jboss.universe:community-universe)#26.1.2.Final</feature-pack-location>
                    <layers>
                        <layer>jaxrs-server</layer>
                    </layers>
                    <plugin-options>
                        <jboss-maven-dist/>
                    </plugin-options>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

WildFly 26.1.2.Finalを使ってBootable JARを作成するようにして、レイヤーはjaxrs-serverを選択。

そして、<jboss-maven-dist/>を追加。

パッケージングしてみる

とりあえず、パッケージングしてみましょう。

$ mvn package

手元の環境では、11秒ほどでパッケージングが終わりました。

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  11.116 s
[INFO] Finished at: 2022-10-22T20:39:03+09:00
[INFO] ------------------------------------------------------------------------

WildFly Bootable JARのサイズは、1.7Mほどです。

$ ll -h target/wildfly-bootable-jar-switch-jboss-maven-dist-0.0-1-SNAPSHOT-bootable.jar
-rw-rw-r-- 1 xxxxx xxxxx 1.7M 10月 22 20:39 target/wildfly-bootable-jar-switch-jboss-maven-dist-0.0-1-SNAPSHOT-bootable.jar

起動。

$ java -jar target/wildfly-bootable-jar-switch-jboss-maven-dist-0.0-1-SNAPSHOT-bootable.jar

起動にかかった時間。

20:40:29,812 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 26.1.2.Final (WildFly Core 18.1.2.Final) started in 3585ms - Started 264 of 349 services (138 services are lazy, passive or on-demand) - Server configuration file in use: standalone.xml

確認。

$ curl localhost:8080/hello
Hello WildFly Bootable JAR!

開発中はwildfly-jar:dev-watchを使うと、ソースコードを変更すると再ビルドが行われてアプリケーションに反映されるので便利です。

$ mvn wildfly-jar:dev-watch
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to /path/to/wildfly-bootable-jar-switch-jboss-maven-dist/target/classes
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO] Exploding webapp
[INFO] Assembling webapp [wildfly-bootable-jar-switch-jboss-maven-dist] in [/path/to/wildfly-bootable-jar-switch-jboss-maven-dist/target/deployments/ROOT.war]
[INFO] Processing war project
20:42:41,902 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 46) WFLYUT0022: Unregistered web context: '/' from server 'default-server'
20:42:41,907 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-3) WFLYSRV0028: Stopped deployment ROOT.war (runtime-name: ROOT.war) in 6ms
20:42:41,917 INFO  [org.jboss.as.server] (management-handler-thread - 1) WFLYSRV0009: Undeployed "ROOT.war" (runtime-name: "ROOT.war")
20:42:41,922 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-2) WFLYSRV0027: Starting deployment of "ROOT.war" (runtime-name: "ROOT.war")
20:42:41,984 INFO  [org.jboss.weld.deployer] (MSC service thread 1-1) WFLYWELD0003: Processing weld deployment ROOT.war
20:42:42,184 INFO  [org.jboss.resteasy.resteasy_jaxrs.i18n] (ServerService Thread Pool -- 48) RESTEASY002225: Deploying javax.ws.rs.core.Application: class org.littlewings.wildfly.bootable.JaxrsActivator
20:42:42,186 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 48) WFLYUT0021: Registered web context: '/' for server 'default-server'
20:42:42,198 INFO  [org.jboss.as.server] (management-handler-thread - 1) WFLYSRV0010: Deployed "ROOT.war" (runtime-name : "ROOT.war")
[INFO] Nothing to compile - all classes are up to date
[INFO] Exploding webapp
[INFO] Assembling webapp [wildfly-bootable-jar-switch-jboss-maven-dist] in [/path/to/wildfly-bootable-jar-switch-jboss-maven-dist/target/deployments/ROOT.war]
[INFO] Processing war project
20:42:42,235 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 48) WFLYUT0022: Unregistered web context: '/' from server 'default-server'
20:42:42,244 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-1) WFLYSRV0028: Stopped deployment ROOT.war (runtime-name: ROOT.war) in 9ms
20:42:42,258 INFO  [org.jboss.as.server] (management-handler-thread - 1) WFLYSRV0009: Undeployed "ROOT.war" (runtime-name: "ROOT.war")
20:42:42,266 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-1) WFLYSRV0027: Starting deployment of "ROOT.war" (runtime-name: "ROOT.war")
20:42:42,314 INFO  [org.jboss.weld.deployer] (MSC service thread 1-4) WFLYWELD0003: Processing weld deployment ROOT.war
20:42:42,441 INFO  [org.jboss.resteasy.resteasy_jaxrs.i18n] (ServerService Thread Pool -- 49) RESTEASY002225: Deploying javax.ws.rs.core.Application: class org.littlewings.wildfly.bootable.JaxrsActivator
20:42:42,444 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 49) WFLYUT0021: Registered web context: '/' for server 'default-server'
20:42:42,450 INFO  [org.jboss.as.server] (management-handler-thread - 1) WFLYSRV0010: Deployed "ROOT.war" (runtime-name : "ROOT.war")

jboss-maven-distを無効にしてみる

次は、jboss-maven-distを無効にしてみましょう。以下のようにコメントアウトしておきます。

                    <plugin-options>
                        <!-- <jboss-maven-dist/> -->
                    </plugin-options>

パッケージング。

$ mvn package

先ほどの2倍ほど時間がかかるようになりました。

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  22.945 s
[INFO] Finished at: 2022-10-22T20:45:41+09:00
[INFO] ------------------------------------------------------------------------

起動してみます。

$ java -jar target/wildfly-bootable-jar-switch-jboss-maven-dist-0.0-1-SNAPSHOT-bootable.jar

この速度は、今回は差がなさそうです。

20:46:16,981 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 26.1.2.Final (WildFly Core 18.1.2.Final) started in 3132ms - Started 264 of 349 services (138 services are lazy, passive or on-demand) - Server configuration file in use: standalone.xml

wildfly-jar:dev-watchを使った場合は、ソースコードの変更を反映する時の差は感じられませんが、最初の起動時はJARファイルを作成する分だけ
差が出ますね。

$ mvn wildfly-jar:dev-watch

というわけで、アプリケーションを作っている時はjboss-maven-distを有効にしたいな、とちょっと思ったりします。

jboss-maven-distにtrue/falseを指定する

で、ドキュメントを見ていると気づかないのですが、jboss-maven-distには以下のようにbooleanの値を指定して有効/無効を切り替えることが
できるようです。

trueとすれば有効になりますし、

                    <plugin-options>
                        <jboss-maven-dist>true</jboss-maven-dist>
                    </plugin-options>

falseで無効になります。

                    <plugin-options>
                        <jboss-maven-dist>false</jboss-maven-dist>
                    </plugin-options>

つまり、以下の2つの記述は同義だということになります。

                    <plugin-options>
                        <jboss-maven-dist/>
                    </plugin-options>


                    <plugin-options>
                        <jboss-maven-dist>true</jboss-maven-dist>
                    </plugin-options>

確認結果は、コメントアウトして切り替えていた時と同じなので割愛。

これを利用すると、プロパティで切り替えたりできそうですね。
こんな感じにして、切り替えたりするとよいのではないでしょうか。

    <properties>
        〜省略〜

        <wildfly.bootable.jar.slim.enable>false</wildfly.bootable.jar.slim.enable>
    </properties>

        〜省略〜

                    <plugin-options>
                        <jboss-maven-dist>${wildfly.bootable.jar.slim.enable}</jboss-maven-dist>
                    </plugin-options>

開発中はjboss-maven-distを有効にして、

$ mvn wildfly-jar:dev-watch -Dwildfly.bootable.jar.slim.enable=true

パッケージングする時には有効にしておくとかでしょうか。

$ mvn package

今回確認したかったのは、ここまでですね。

最後に、この修正を入れたpom.xmlを載せておきます。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>wildfly-bootable-jar-switch-jboss-maven-dist</artifactId>
    <version>0.0-1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <failOnMissingWebXml>false</failOnMissingWebXml>

        <wildfly.bootable.jar.slim.enable>false</wildfly.bootable.jar.slim.enable>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>8.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-jar-maven-plugin</artifactId>
                <version>8.0.1.Final</version>
                <configuration>
                    <feature-pack-location>wildfly@maven(org.jboss.universe:community-universe)#26.1.2.Final</feature-pack-location>
                    <layers>
                        <layer>jaxrs-server</layer>
                    </layers>
                    <plugin-options>
                        <jboss-maven-dist>${wildfly.bootable.jar.slim.enable}</jboss-maven-dist>
                    </plugin-options>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

まとめ

WildFly Bootable JARを作る時に使う、WildFly JAR Maven Pluginのjboss-maven-distの有効/無効を切り替えられることを確認してみました。

個人的にWildFly Bootable JARは便利で、Java EE/Jakarta EEアプリケーション作成時の確認の際にデプロイせずとも良いですし、
wildfly-jar:dev-watchがソースコードの変更を反映してくれるので楽です。

最終的にWildFlyにデプロイする場合は、WildFly Bootable JARを作成するかどうかはMavenのプロファイルで分けたりすればよいと思うので
用途に合わせて使い分けていきたいですね。