CLOVER🍀

That was when it all began.

Molecule 3を試す

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

Moleculeが3.0になっていたので、1度試しておこうかなと。

前に試した時は、2.22でした。

Ansible Roleを開発、テストするためのMoleculeを試す - CLOVER🍀

Changelogを見ると、だいぶ変わったみたいです。

Changelog / 3.0

特に大きな変更は、以下あたりです(Changelogの「MAJOR」から抜粋)。

  • Ansibleのバージョンサポート範囲がN/N-1(たとえば2.9と2.8)になる
  • Providerはインストール可能なドライバー、Pythonモジュールになった
  • コアから、Azure、EC2、DigitalOcean、GCE、HetznerCloud、Linode、LXD、OpenStack、Vagrantの各Providerが削除
  • goss verifierを削除
  • playbook.ymlという名前は非推奨になり、converge.ymlに
  • デフォルトのverifierがAnsibleになった
  • Testinfraはデフォルトでインストールされなくなった
  • Scenerio名が設定から削除された
  • Lintingの設定がリファクタリングされた
  • DockerfileのテンプレートがMolecureに組み込み可能になった

Migrating to molecule v3 checklist · Issue #2560 · ansible-community/molecule · GitHub

Providerがだいぶ削除されたり、Testinfraが入らなくなったりしたんですねぇ。

とりあえず、今回は2.22の時に試した内容を、3.0に読み替えてやってみようと思います。

Moleculeとは

少しだけ、振り返っておきます。

Ansible Molecule — Molecule 3.0.3.dev73+gcc0c7634 documentation

Moleculeは、Ansible Roleを作ったりテストするためのプロジェクトです。

Molecule project is designed to aid in the development and testing of Ansible roles.

複数のインスタンス、OS、仮想化プロバイダー、テストに関するサポートを提供するものです。

Molecule provides support for testing with multiple instances, operating systems and distributions, virtualization providers, test frameworks and testing scenarios.

環境

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

$ python3 -V
Python 3.6.9


$ pip3 -V
pip 9.0.1 from /path/to/venv/lib/python3.6/site-packages (python 3.6)

インストール

まずは、Moleculeをインストールします。

Installation — Molecule 3.0.3.dev73+gcc0c7634 documentation

$ pip3 install molecule[lint]

インストールしたバージョン。

$ pip3 freeze | grep -E 'ansible|molecule'
ansible==2.9.6
ansible-lint==4.2.0
molecule==3.0.2

試してみる

Getting Startedを見つつ、Moleculeを試していきます。

Getting Started Guide — Molecule 3.0.3.dev73+gcc0c7634 documentation

お題としては、ApacheをインストールするRoleを作ることにしましょう。

Creating a new role

「molecule init」でRoleを作成。

$ molecule init role apache

できあがったファイル。

$ find apache -type f
apache/handlers/main.yml
apache/README.md
apache/vars/main.yml
apache/.travis.yml
apache/meta/main.yml
apache/defaults/main.yml
apache/tests/inventory
apache/tests/test.yml
apache/tasks/main.yml
apache/molecule/default/verify.yml
apache/molecule/default/converge.yml
apache/molecule/default/molecule.yml
apache/molecule/default/INSTALL.rst
apache/.yamllint

いくつか、見ていきましょう。

定義内容は、Configuration Guideを見つつ。

Configuration — Molecule 3.0.3.dev73+gcc0c7634 documentation

Moleculeの中心となる設定ファイルです。
apache/molecule/default/molecule.yml

---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: docker.io/pycontribs/centos:7
    pre_build_image: true
provisioner:
  name: ansible
verifier:
  name: ansible

Changelogにもあったとおり、playbook.ymlはconverge.ymlになったんですね。
apache/molecule/default/converge.yml

---
- name: Converge
  hosts: all
  tasks:
    - name: "Include apache"
      include_role:
        name: "apache"

デフォルトのVerifierがAnsibleになったので、verify.ymlというファイルが生成されています。
apache/molecule/default/verify.yml

---
# This is an example playbook to execute Ansible tests.

- name: Verify
  hosts: all
  tasks:
  - name: Example assertion
    assert:
      that: true

assertモジュールで検証します、と。

assert – Asserts given expressions are true — Ansible Documentation

作成されたRoleのディレクトリに移動。

$ cd apache

DockerイメージはpysのCentOSでしたが、今回はUbuntu Linuxを使うことにしましょう。

GitHub - pycontribs/pys: Repository used to pre-build python enabled images for popular operating systems

pys / ubuntu

pysというのは、メジャーなOSでPythonを有効化してビルドされたDockerイメージです。

こんな感じに変更。

driver:
  name: docker
platforms:
  - name: instance
    image: docker.io/pycontribs/ubuntu:latest
    pre_build_image: true
    dns_servers:
      - 8.8.8.8

dns_servers」を指定しているのは、aptでApacheをインストールする時に名前解決ができなくてエラーになったからです…。

Err:1 http://archive.ubuntu.com/ubuntu bionic InRelease                  
  Temporary failure resolving 'archive.ubuntu.com'
Err:2 http://security.ubuntu.com/ubuntu bionic-security InRelease        
  Temporary failure resolving 'security.ubuntu.com'
Err:3 http://archive.ubuntu.com/ubuntu bionic-updates InRelease          
  Temporary failure resolving 'archive.ubuntu.com'
Err:4 http://archive.ubuntu.com/ubuntu bionic-backports InRelease
  Temporary failure resolving 'archive.ubuntu.com'

では、プロビジョニングを行うインスタンスを作成します。

Run test sequence commands

「molecule create」でインスタンスの作成…ですが、エラーになります。

$ molecule create
--> Test matrix
    
└── default
    ├── dependency
    ├── create
    └── prepare
    
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'create'
--> Sanity checks: 'docker'
ERROR: Missing Docker driver dependency. Please install via 'molecule[docker]' or refer to your INSTALL.rst driver documentation file

「Dockerドライバがない」と言われているので、インストール。

$ pip3 install molecule[docker]

バージョン。

$ pip3 freeze | grep -E 'ansible|molecule|docker'
ansible==2.9.6
ansible-lint==4.2.0
docker==4.2.0
molecule==3.0.2

再度、「molecule create」。

$ molecule create

今度は成功します。

$ molecule create
--> Test matrix
    
└── default
    ├── dependency
    ├── create
    └── prepare
    
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'create'
--> Sanity checks: 'docker'
    
    PLAY [Create] ******************************************************************
    
    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None) 
    
    TASK [Check presence of custom Dockerfiles] ************************************
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Create Dockerfiles from image names] *************************************
    skipping: [localhost] => (item=None) 
    
    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Build an Ansible compatible image (new)] *********************************
    skipping: [localhost] => (item=molecule_local/docker.io/pycontribs/ubuntu:latest) 
    
    TASK [Create docker network(s)] ************************************************
    
    TASK [Determine the CMD directives] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=instance)
    
    TASK [Wait for instance(s) creation to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (299 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (298 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (297 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (296 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (295 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (294 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (293 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (292 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (291 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (290 retries left).
    FAILED - RETRYING: Wait for instance(s) creation to complete (289 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0
    
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.

インスタンスの一覧を確認。

$ molecule list
Instance Name    Driver Name    Provisioner Name    Scenario Name    Created    Converged
---------------  -------------  ------------------  ---------------  ---------  -----------
instance         docker         ansible             default          true       false

当然のことながら、dockerコマンドでも確認できます。

$ docker container ps
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS               NAMES
ba6732094f07        pycontribs/ubuntu:latest   "bash -c 'while true…"   56 seconds ago      Up 40 seconds                           instance

起動中のインスタンスにログインするには、「molecule login」で。

$ molecule login
root@instance:/# 

Ubuntu Linux 18.04 LTSですね。

# uname -a
Linux instance 4.18.0-25-generic #26~18.04.1-Ubuntu SMP Thu Jun 27 07:28:31 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux


# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.4 LTS
Release:    18.04
Codename:   bionic

インスタンスの破棄。

$ molecule destroy

破棄の様子。

$ molecule destroy
--> Test matrix
    
└── default
    ├── dependency
    ├── cleanup
    └── destroy
    
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
--> Sanity checks: 'docker'
    
    PLAY [Destroy] *****************************************************************
    
    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=instance)
    
    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    TASK [Delete docker network(s)] ************************************************
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
    
--> Pruning extra files from scenario ephemeral directory

では、ApacheをインストールするようにAnsible Roleを作成しましょう。tasks/main.ymlに、以下のように記述。
tasks/main.yml

---
- name: install apache2
  apt:
    name: apache2
    state: present
    update_cache: yes

「molecule converge」で、インスタンスの作成とAnsible Playbookの実行を行います。

$ molecule converge

実行されるのは、coverage.ymlの内容ですね。
molecule/default/converge.yml

---
- name: Converge
  hosts: all
  tasks:
    - name: "Include apache"
      include_role:
        name: "apache"

Playbookが実行されている様子。

--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'
    
    PLAY [Converge] ****************************************************************
    
    TASK [Gathering Facts] *********************************************************
    ok: [instance]
    
    TASK [Include apache] **********************************************************
    
    TASK [apache : install apache2] ************************************************
    changed: [instance]
    
    PLAY RECAP *********************************************************************
    instance                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

次は、テスト(Verify)を行ってみましょう。

Verifier

ところで、MoleculeのデフォルトのVerifierはTestinfraからAnsibleに変わったようですが、それはどうしてかちょっと見てみました。

Make Ansible default verifier · Issue #2013 · ansible-community/molecule · GitHub

使うテクノロジーを、YAMLに統一したかったみたいですね。でも、AnsibleのassertはTestinfraと比べるとどうなのという話もあり…。

デフォルトのverify.ymlを、もう1度見てみます。
molecule/default/verify.yml

---
# This is an example playbook to execute Ansible tests.

- name: Verify
  hosts: all
  tasks:
  - name: Example assertion
    assert:
      that: true

ここからどう変えていいかちょっとわからなくなったので、今回はTestinfraに切り替えることにしました。

Testinfraとpytestをインストールします。

$ pip3 install testinfra pytest

バージョン。

 pip3 freeze | grep -E 'testinfra|pytest'
pytest==5.4.1
testinfra==5.0.0

molecule.ymlのVerifierの設定を、AnsibleからTestinfraへ切り替えます。

verifier:
#  name: ansible
  name: testinfra

テストコードを置くディレクトリを作成。

$ mkdir molecule/default/tests

作成したテストコード。
molecule/default/tests/test_apache.py

import os

import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
   os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')

def test_apache_is_installed(host):
    apache2 = host.package('apache2')
    assert apache2.is_installed

インスタンスが起動している状態であれば、「molecule verify」でテストを行うことができます。

$ molecule verify

こんな感じで。

$ molecule verify
--> Test matrix
    
└── default
    └── verify
    
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /path/to/apache/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
    rootdir: /path/to/apache/molecule/default
    plugins: testinfra-5.0.0
collected 1 item                                                               
    
    tests/test_default.py .                                                  [100%]
    
    ============================== 1 passed in 5.26s ===============================
Verifier completed successfully.

終わったら、インスタンスを破棄しておきましょう。

$ molecule destroy

ここまでの、インスタンスの作成、Playbookの実行、テスト、インスタンスの破棄まで一気に行う場合は、「molecule test」を
実行します。

Run a full test sequence

$ molecule test

こんな感じで、Moleculeのイントロ的な内容を、ざっくり確認してみました。

Molecule 2から3で、テストまわりが変わっていたり、ドライバがなくなっていたりでなかなかビックリしました。

Infinispan 10.1のCacheLoader/CacheWriter(ExternalStore)を試す

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

最近、久しぶりにInfinispanのCacheStoreまわりのAPIをちょっと眺めてみたら、特にAdvancedCacheLoader/AdvancedCacheWriterまわりが
だいぶ変わっているのに驚きまして。

いい機会なので、もう1度CacheStoreまわりのAPIを見ていこうかなと。

いきなりAdvanced〜の方にいくとハードルが高いので、今回はまずCacheLoader/CacheWriterから使っていきます。

Persistence

こちらは、Infinispan 6.0の頃に書いたエントリです。

InfinispanのCacheLoader/CacheWriterを使ってみる - CLOVER🍀

今回はInfinispan 10.1で見直してみます。

Setting Up Persistent Storage

InfinispanからいくつかCacheStoreの実装は提供されていますが、今回は自分でCacheStoreを実装する方を見ていきます。
これが、Infinispan Persistence SPIで、外部ストレージへの読み書きを行うためのAPIとなります。

Infinispan Persistence SPIs

Infinispan Persistence SPIは、以下の特徴を持つと書かれています。

  • 標準に準拠 … JSR-107仕様に準拠したCacheWriter、CacheLoaderインターフェース(※1)
  • トランザクションへの統合 … Infinispan側でロックを扱うため、CacheStoreの実装は永続ストアへの同時アクセスを考慮しなくてよい(※2)
  • Parallel Iteration … マルチスレッドを使った並列処理で永続ストアにあるエントリを扱うことができる
  • シリアライズの削減 … Infinispanは保存されたエントリを、リモートに送信可能なシリアライズされた形式で公開しているため、永続ストアからの取得時のデシリアライズ、書き込み時の再度のシリアライズは不要

※1 … そう?
※2 … 永続ストア自体は、複数のスレッドからアクセスされることを許容する必要がある

Infinispan Persistence SPIは、以下のAPIから構成されます。

  • ByteBuffer … シリアライズされたオブジェクトを抽象化したもの
  • MarshallableEntry … Cacheに登録されたキー/値を永続ストア内に保持するために抽象化したもので、シリアライズ/デシリアライズされた形式の両方が扱える。なお、シリアライズは遅延処理される
  • CacheWriter/CacheLoader … 永続ストアへの読み書きに対する基本的な機能を実現
  • AdvancedCacheLoader/AdvancedCacheWriter … 並列でのイテレーション、有効期限切れエントリのパージ、クリア、サイズ取得といった、永続ストレージに対するバルク操作を実現
  • SegmentedAdvancedLoadWriteStore … セグメントを扱う操作を実現

今回はCacheLoaderおよびCacheWriterを扱うのですが、実際に使うにはAdvancedCacheLoader/AdvancedCacheWriterがないと
機能的には足りないことがわかります。AdvancedCacheLoaderを実装していない場合はイテレーションができず、Expireにも対応
できません。AdvancedCacheWriterを実装していない場合は、有効期限切れのエントリをパージしたりクリアもできません。

なのですが、Advanced〜に関するAPIは割と難しいので、また機会を改めて。

あと、SegmentedAdvancedLoadWriteStoreが追加されていたのは知らなかったですね。Infinispan 9.4からのようです。

では、説明はこれくらいにして、今回はCacheLoader/CacheWriterを使っていってみます。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.6 2020-01-14
OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing)


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

準備

Maven依存関係は、こちら。

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

        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <version>1.6.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.15.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Infinispanは10.1.5.Finalを使用します。また、JUnit 5とAssertJはテストコード用です。

お題

今回は、CacheStoreが使う永続ストレージとしてMap(永続じゃないですが)を使うことにします。

極めて簡易なものですが、とりあえず基本的な使い方ということで。

作成するものは、設定まわりでStoreConfiguration、StoreConfigurationBuilderの実装、そして永続化を扱うためにCacheLoader/CacheWriterの
実装です。

設定まわりのクラスを作る

では、最初に設定まわりのクラスを作成します。

StoreConfiguration (Infinispan JavaDoc All 10.1.5.Final API)

StoreConfigurationBuilder (Infinispan JavaDoc All 10.1.5.Final API)

StoreConfigurationの実装。
src/main/java/org/littlewings/infinispan/persistence/SimpleMapCacheStoreConfiguration.java

package org.littlewings.infinispan.persistence;

import org.infinispan.commons.configuration.BuiltBy;
import org.infinispan.commons.configuration.ConfigurationFor;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.configuration.cache.AbstractStoreConfiguration;
import org.infinispan.configuration.cache.AsyncStoreConfiguration;

@BuiltBy(SimpleMapCacheStoreConfigurationBuilder.class)
@ConfigurationFor(SimpleMapCacheStore.class)
public class SimpleMapCacheStoreConfiguration extends AbstractStoreConfiguration {
    public SimpleMapCacheStoreConfiguration(AttributeSet attributes, AsyncStoreConfiguration async) {
        super(attributes, async);
    }
}

@BuildByアノテーションでStoreConfigurationBuilderの実装を、@ConfigurationForアノテーションでCacheStoreの実装を指定します。

StoreConfigurationBuilderは、CacheStoreの設定を行うためのBuilderですね。Infinispanの設定をAPIで行おうとすると、
ConfigurationBuilderからbuildしてConfigurationを作るという流れが登場するのですが、自作のCacheStore用に作成している
感じです。

StoreConfigurationBuilder側。
src/main/java/org/littlewings/infinispan/persistence/SimpleMapCacheStoreConfigurationBuilder.java

package org.littlewings.infinispan.persistence;

import org.infinispan.commons.configuration.ConfigurationBuilderInfo;
import org.infinispan.configuration.cache.AbstractStoreConfiguration;
import org.infinispan.configuration.cache.AbstractStoreConfigurationBuilder;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;

public class SimpleMapCacheStoreConfigurationBuilder extends AbstractStoreConfigurationBuilder<SimpleMapCacheStoreConfiguration, SimpleMapCacheStoreConfigurationBuilder> implements ConfigurationBuilderInfo {
    public SimpleMapCacheStoreConfigurationBuilder(PersistenceConfigurationBuilder builder) {
        super(builder, AbstractStoreConfiguration.attributeDefinitionSet());
    }

    @Override
    public SimpleMapCacheStoreConfiguration create() {
        return new SimpleMapCacheStoreConfiguration(attributes.protect(), async.create());
    }

    @Override
    public SimpleMapCacheStoreConfigurationBuilder self() {
        return this;
    }
}

クラス宣言時のジェネリクスがややこしいですが、まあ、こんな感じです。

なお、StoreConfigurationもStoreConfigurationBuilderも、最低限の実装です。この内容なら、実は作成しなくてもよいくらいですが、
それは後で少し紹介します。

設定とかをちゃんと定義したい場合は、既存のCacheStoreを参考にしましょう。

https://github.com/infinispan/infinispan/tree/10.1.5.Final/core/src/main/java/org/infinispan/configuration/cache

https://github.com/infinispan/infinispan/tree/10.1.5.Final/core/src/main/java/org/infinispan/persistence/file

https://github.com/infinispan/infinispan/tree/10.1.5.Final/core/src/main/java/org/infinispan/persistence/cluster

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/soft-index

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/rocksdb

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/jdbc

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/jpa

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/remote

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/rest

CacheLoader/CacheWriterの実装を作る

続いて、先ほどのStoreConfiguration、StoreConfigurationBuilderにより作成される、CacheLoader/CacheWriterの実装クラスを
作成します。

CacheLoader (Infinispan JavaDoc All 10.1.5.Final API)

CacheWriter (Infinispan JavaDoc All 10.1.5.Final API)

この2つのインターフェースを実装したクラスを作成するのですが、今回はこの2つのインターフェースを継承したExternalStore
インターフェースを実装することにします。

ExternalStore (Infinispan JavaDoc All 10.1.5.Final API)

で、作ったのはこんな感じ。
src/main/java/org/littlewings/infinispan/persistence/SimpleMapCacheStore.java

package org.littlewings.infinispan.persistence;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.infinispan.commons.configuration.ConfiguredBy;
import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.commons.marshall.Marshaller;
import org.infinispan.commons.persistence.Store;
import org.infinispan.configuration.cache.CustomStoreConfiguration;
import org.infinispan.persistence.spi.ExternalStore;
import org.infinispan.persistence.spi.InitializationContext;
import org.infinispan.persistence.spi.MarshallableEntry;
import org.infinispan.persistence.spi.MarshallableEntryFactory;
import org.infinispan.persistence.spi.PersistenceException;
import org.jboss.logging.Logger;

@Store
@ConfiguredBy(SimpleMapCacheStoreConfiguration.class)
public class SimpleMapCacheStore<K, V> implements ExternalStore<K, V> {
    Logger logger = Logger.getLogger(getClass());

    SimpleMapCacheStoreConfiguration storeConfiguration;
    //CustomStoreConfiguration storeConfiguration;
    String storeName;

    Map<ByteBuffer, EntryData> store;

    InitializationContext ctx;

    @Override
    public void init(InitializationContext ctx) {
        this.ctx = ctx;
        storeConfiguration = ctx.getConfiguration();

        if (storeConfiguration.properties().getProperty("storeName") != null) {
            storeName = storeConfiguration.properties().getProperty("storeName");
        } else {
            storeName = "defaultStoreName";
        }

        logging("initialized");
    }

    @Override
    public void write(MarshallableEntry<? extends K, ? extends V> entry) {
        ByteBuffer keyAsBinary = entry.getKeyBytes();
        ByteBuffer valueAsBinary = entry.getValueBytes();

        long created = entry.created();
        long expiryTime = entry.expiryTime();
        long lastUsed = entry.lastUsed();

        ByteBuffer metadataAsBinary = entry.getMetadataBytes();

        EntryData entryData = EntryData.create(valueAsBinary, created, expiryTime, lastUsed, metadataAsBinary);

        store.put(keyAsBinary, entryData);

        logging(
                "write entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                entry.getKey(),
                entry.getValue(),
                entry.created(),
                entry.expiryTime(),
                entry.lastUsed(),
                entry.getMetadata()
        );

    }

    @Override
    public MarshallableEntry<K, V> loadEntry(Object key) {
        MarshallableEntryFactory<K, V> marshallableEntryFactory = ctx.getMarshallableEntryFactory();
        Marshaller marshaller = ctx.getPersistenceMarshaller().getUserMarshaller();

        try {
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData entryData = store.get(keyAsBinary);

            if (entryData != null) {
                MarshallableEntry<K, V> marshallableEntry =
                        marshallableEntryFactory.create(keyAsBinary, entryData.valueAsBinry, entryData.metadataAsBinary, entryData.getCreated(), entryData.getLastUsed());

                logging(
                        "load entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                        marshallableEntry.getKey(),
                        marshallableEntry.getValue(),
                        marshallableEntry.created(),
                        marshallableEntry.expiryTime(),
                        marshallableEntry.lastUsed(),
                        marshallableEntry.getMetadata()
                );
                return marshallableEntry;
            } else {
                logging("missing entry: %s", key);
                return null;
            }
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    @Override
    public boolean delete(Object key) {
        try {
            Marshaller marshaller = ctx.getPersistenceMarshaller();
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData deleted = store.remove(keyAsBinary);

            logging("deleted? %b, key = %s", deleted != null, key);

            return deleted != null;
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    @Override
    public boolean contains(Object key) {
        try {
            Marshaller marshaller = ctx.getPersistenceMarshaller();
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);

            boolean contains = store.containsKey(keyAsBinary);

            logging("contains? %b, key = %s", contains, key);

            return contains;
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    @Override
    public void start() {
        store = new ConcurrentHashMap<>();

        logging("cache store, startup");
    }

    @Override
    public void stop() {
        store.clear();

        logging("cache store, stopped");
    }

    void logging(String messageTemplate, Object... params) {
        List<Object> mergeParams = new ArrayList<>();
        mergeParams.add(ctx.getCache().getAdvancedCache().getDistributionManager().getCacheTopology().getLocalAddress());
        mergeParams.add(getClass().getSimpleName());
        mergeParams.add(storeName);
        mergeParams.addAll(Arrays.asList(params));

        logger.infof("[%s - %s - %s] " + messageTemplate, mergeParams.toArray());
    }
}

データの持ち先は、Mapにします。

    Map<ByteBuffer, EntryData> store;

簡単にConcurrentHashMapということで。ライフサイクルメソッドで、初期化とクリアを仕込み。

    @Override
    public void start() {
        store = new ConcurrentHashMap<>();

        logging("cache store, startup");
    }

    @Override
    public void stop() {
        store.clear();

        logging("cache store, stopped");
    }

initは、CacheLoader/CacheWriterの初期化メソッドです。この引数で受け取るInitializationContextから、設定やMarshallerなどを
取得することができます。

    @Override
    public void init(InitializationContext ctx) {
        this.ctx = ctx;
        storeConfiguration = ctx.getConfiguration();

        if (storeConfiguration.properties().getProperty("storeName") != null) {
            storeName = storeConfiguration.properties().getProperty("storeName");
        } else {
            storeName = "defaultStoreName";
        }

        logging("initialized");
    }

また、今回はストアの名前をプロパティとして受け取ることにしました。

CacheStore操作時のログは、Node名、クラス名、プロパティで指定されたストア名を出力するように実装。

    void logging(String messageTemplate, Object... params) {
        List<Object> mergeParams = new ArrayList<>();
        mergeParams.add(ctx.getCache().getAdvancedCache().getDistributionManager().getCacheTopology().getLocalAddress());
        mergeParams.add(getClass().getSimpleName());
        mergeParams.add(storeName);
        mergeParams.addAll(Arrays.asList(params));

        logger.infof("[%s - %s - %s] " + messageTemplate, mergeParams.toArray());
    }

エントリの書き込み、読み込み、削除。

    @Override
    public void write(MarshallableEntry<? extends K, ? extends V> entry) {
        ByteBuffer keyAsBinary = entry.getKeyBytes();
        ByteBuffer valueAsBinary = entry.getValueBytes();

        long created = entry.created();
        long expiryTime = entry.expiryTime();
        long lastUsed = entry.lastUsed();

        ByteBuffer metadataAsBinary = entry.getMetadataBytes();

        EntryData entryData = EntryData.create(valueAsBinary, created, expiryTime, lastUsed, metadataAsBinary);

        store.put(keyAsBinary, entryData);

        logging(
                "write entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                entry.getKey(),
                entry.getValue(),
                entry.created(),
                entry.expiryTime(),
                entry.lastUsed(),
                entry.getMetadata()
        );

    }

    @Override
    public MarshallableEntry<K, V> loadEntry(Object key) {
        MarshallableEntryFactory<K, V> marshallableEntryFactory = ctx.getMarshallableEntryFactory();
        Marshaller marshaller = ctx.getPersistenceMarshaller().getUserMarshaller();

        try {
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData entryData = store.get(keyAsBinary);

            if (entryData != null) {
                MarshallableEntry<K, V> marshallableEntry =
                        marshallableEntryFactory.create(keyAsBinary, entryData.valueAsBinry, entryData.metadataAsBinary, entryData.getCreated(), entryData.getLastUsed());

                logging(
                        "load entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                        marshallableEntry.getKey(),
                        marshallableEntry.getValue(),
                        marshallableEntry.created(),
                        marshallableEntry.expiryTime(),
                        marshallableEntry.lastUsed(),
                        marshallableEntry.getMetadata()
                );
                return marshallableEntry;
            } else {
                logging("missing entry: %s", key);
                return null;
            }
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    @Override
    public boolean delete(Object key) {
        try {
            Marshaller marshaller = ctx.getPersistenceMarshaller();
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData deleted = store.remove(keyAsBinary);

            logging("deleted? %b, key = %s", deleted != null, key);

            return deleted != null;
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

MarshallableEntryというクラスが、マーシャリングした形式のデータを持っているので、こちらを永続ストア(今回はMapですが)に
保存します。

    @Override
    public void write(MarshallableEntry<? extends K, ? extends V> entry) {
        ByteBuffer keyAsBinary = entry.getKeyBytes();
        ByteBuffer valueAsBinary = entry.getValueBytes();

        long created = entry.created();
        long expiryTime = entry.expiryTime();
        long lastUsed = entry.lastUsed();

        ByteBuffer metadataAsBinary = entry.getMetadataBytes();

        EntryData entryData = EntryData.create(valueAsBinary, created, expiryTime, lastUsed, metadataAsBinary);

なお、EntryDataというのは値と各種メタデータを保存するために、今回作成したクラスです。
src/main/java/org/littlewings/infinispan/persistence/EntryData.java

package org.littlewings.infinispan.persistence;

import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.metadata.Metadata;

public class EntryData {
    ByteBuffer valueAsBinry;
    long created;
    long expiryTime;
    long lastUsed;
    ByteBuffer metadataAsBinary;

    public static EntryData create(ByteBuffer valueAsBinary, long created, long expiryTime, long lastUsed, ByteBuffer metadataAsBinary) {
        EntryData data = new EntryData();
        data.valueAsBinry = valueAsBinary;
        data.created = created;
        data.expiryTime = expiryTime;
        data.lastUsed = lastUsed;
        data.metadataAsBinary = metadataAsBinary;

        return data;
    }

    // getter/setterは省略
}

読み込みの場合は、キーが渡ってくるのでMarshallerおよびMarshallableEntryFactoryを使ってMarshallableEntryにデータを変換してから
返却します。

    @Override
    public MarshallableEntry<K, V> loadEntry(Object key) {
        MarshallableEntryFactory<K, V> marshallableEntryFactory = ctx.getMarshallableEntryFactory();
        Marshaller marshaller = ctx.getPersistenceMarshaller().getUserMarshaller();

        try {
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData entryData = store.get(keyAsBinary);

            if (entryData != null) {
                MarshallableEntry<K, V> marshallableEntry =
                        marshallableEntryFactory.create(keyAsBinary, entryData.valueAsBinry, entryData.metadataAsBinary, entryData.getCreated(), entryData.getLastUsed());

                logging(
                        "load entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                        marshallableEntry.getKey(),
                        marshallableEntry.getValue(),
                        marshallableEntry.created(),
                        marshallableEntry.expiryTime(),
                        marshallableEntry.lastUsed(),
                        marshallableEntry.getMetadata()
                );
                return marshallableEntry;

ここで返したMarshallableEntryは、DataContainer側で保持されることになります。

削除については、省略。containsは実装はしていますが、呼ばれる気がしませんが…?

確認

では、テストコードで確認してみましょう。といっても、CacheStoreの動作自体はログで見るのですが。

テストコードの雛形。
src/test/java/org/littlewings/infinispan/persistence/SimpleMapStoreTest.java

package org.littlewings.infinispan.persistence;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.junit.jupiter.api.Test;

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

public class SimpleMapStoreTest {
    <K, V> void withCache(String cacheName, int numInstances, Consumer<Cache<K, V>> func) {
        List<EmbeddedCacheManager> managers =
                IntStream
                        .rangeClosed(1, numInstances)
                        .mapToObj(i -> {
                                    try {
                                        return new DefaultCacheManager("infinispan.xml");
                                    } catch (IOException e) {
                                        throw new UncheckedIOException(e);
                                    }
                                }
                        )
                        .collect(Collectors.toList());

        try {
            managers.forEach(m -> m.getCache(cacheName));

            Cache<K, V> cache = managers.get(0).getCache(cacheName);
            func.accept(cache);
        } finally {
            managers.forEach(EmbeddedCacheManager::stop);
        }
    }

    // ここに、テストを書く!
}

簡単にEmbeddedなクラスタを作成できるメソッド付きです。

Infinispanの設定。
src/test/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="urn:infinispan:config:10.1 https://infinispan.org/schemas/infinispan-config-10.1.xsd"
        xmlns="urn:infinispan:config:10.1">
    <cache-container>
        <transport stack="udp"/>

        <distributed-cache name="declarativeSimpleStoreCache">
            <persistence>
                <store class="org.littlewings.infinispan.persistence.SimpleMapCacheStore">
                    <property name="storeName">declarativeStore</property>
                </store>
            </persistence>
        </distributed-cache>
    </cache-container>
</infinispan>

persistence/storeで、自分で作成したCacheStoreの実装クラスを指定。プロパティも設定しておきます。

            <persistence>
                <store class="org.littlewings.infinispan.persistence.SimpleMapCacheStore">
                    <property name="storeName">declarativeStore</property>
                </store>
            </persistence>

まずは、設定ファイルで定義したCacheを使ってみます。

    @Test
    public void withDeclarativeSimpleCacheStore() {
        this.<String, String>withCache("declarativeSimpleStoreCache", 3, cache -> {
            System.out.println("============================== start ==============================");

            System.out.println("[Data Put] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.put("key" + i, "value" + i));
            System.out.println("[Data Put] end");

            System.out.println("[Data Get] start");
            assertThat(cache.get("key1")).isEqualTo("value1");
            System.out.println("[Data Get] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Remove] start");
            cache.remove("key3");
            System.out.println("[Data Remove] end");

            System.out.println("[Data Clear] start");
            cache.clear();
            System.out.println("[Data Clear] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Get] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.get("key" + i));
            System.out.println("[Data Get] end");

            System.out.println("============================== end ==============================");
        });
    }

ログを追ってみてみます。

まずはデータのput。

============================== start ==============================
[Data Put] start
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key1
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key2
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key2 / value2, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key2 / value2, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] missing entry: key3
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key3 / value3, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key3 / value3, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] missing entry: key4
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key4 / value4, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key4 / value4, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] missing entry: key5
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key5 / value5, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key5 / value5, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key6
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key6 / value6, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key6 / value6, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key7
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key7 / value7, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key7 / value7, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key8
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key8 / value8, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key8 / value8, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] missing entry: key9
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key9 / value9, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key9 / value9, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key10
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key10 / value10, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key10 / value10, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
[Data Put] end

CacheStoreに書き込みする前に、1度データをロードしようとしていますね。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key1

書き込みは、2回発生。バックアップ分があるからですね。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}

この状態でデータを取得すると、CacheStore側までアクセスが来ません。

[Data Get] start
[Data Get] end

InfinispanのDataContainer側(InfinispanのCacheの内部)にデータがある場合は、CacheStoreまで来ないからです。

削除。こちらはCacheStoreまでアクセスが来ます。

[Data Remove] start
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] deleted? true, key = key3
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] deleted? true, key = key3
[Data Remove] end

ここで、1度データをクリアします。この時、CacheStore側にはなにも起こりません(起こしたい場合はAdvanced〜が必要)。

[Data Clear] start
[Data Clear] end

この状態だと、DataContainerにはデータはないけれど、CacheStoreにはデータが残ったまま(明示的に削除したものを除く)
という状態になっているので、これを利用してデータをCacheStoreからロードしてみます。

1件取得。データをCacheStoreからロードしに来たことがわかります。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key2 / value2, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}

ループで取得。

[Data Get] start
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] missing entry: key3
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] load entry: key4 / value4, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] load entry: key5 / value5, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] load entry: key6 / value6, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key7 / value7, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key8 / value8, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] load entry: key9 / value9, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key10 / value10, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
[Data Get] end
============================== end ==============================

データの取得の場合は、バックアップ分は動かないんですね。1回しかCacheStoreが動いていません。

あとは、ログにプロパティで指定したCacheStoreの名前が反映されていることを確認。

INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore]

続いて、CacheをAPIで定義する時にCacheStoreを設定する方法を見てみましょう。

    @Test
    public void withProgrammaticallyDeclarativeSimpleCacheStore() throws IOException {
        try (EmbeddedCacheManager manager = new DefaultCacheManager("infinispan.xml")) {

            PersistenceConfigurationBuilder persistenceConfigurationBuilder =
                    new ConfigurationBuilder()
                            .clustering()
                            .cacheMode(CacheMode.DIST_SYNC)
                            .persistence();

            Configuration configuration =
                    persistenceConfigurationBuilder
                            .addStore(new SimpleMapCacheStoreConfigurationBuilder(persistenceConfigurationBuilder))
                            .addProperty("storeName", "programmaticallyStore")
                            .build();

            manager.defineConfiguration("programmaticallySimpleStoreCache", configuration);

            Cache<String, String> cache = manager.getCache("programmaticallySimpleStoreCache");

            System.out.println("============================== start ==============================");

            System.out.println("[Data Put] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.put("key" + i, "value" + i));
            System.out.println("[Data Put] end");

            System.out.println("[Data Get] start");
            assertThat(cache.get("key1")).isEqualTo("value1");
            System.out.println("[Data Get] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Remove] start");
            cache.remove("key3");
            System.out.println("[Data Remove] end");

            System.out.println("[Data Clear] start");
            cache.clear();
            System.out.println("[Data Clear] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Get] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.get("key" + i));
            System.out.println("[Data Get] end");

            System.out.println("============================== end ==============================");
        }
    }

テストコード側は先ほどと同じなので省略しますが、CacheStoreの設定方法はこんな感じですね。

            PersistenceConfigurationBuilder persistenceConfigurationBuilder =
                    new ConfigurationBuilder()
                            .clustering()
                            .cacheMode(CacheMode.DIST_SYNC)
                            .persistence();

            Configuration configuration =
                    persistenceConfigurationBuilder
                            .addStore(new SimpleMapCacheStoreConfigurationBuilder(persistenceConfigurationBuilder))
                            .addProperty("storeName", "programmaticallyStore")
                            .build();

            manager.defineConfiguration("programmaticallySimpleStoreCache", configuration);

ログにプロパティの内容が反映されていることを確認。

INFO: [xxxxx-30918 - SimpleMapCacheStore - declarativeStore]

OKそうですね。これで、確認できました、と。

あとは、少しオマケを書いていきます。

CustomStoreConfiguration

先に少し書きましたが、今回はCacheStoreの設定としてプロパティくらいしか使わないので、実はStoreConfigurationは作る必要は
なかったりします。

今回のCacheStoreの実装では、@ConfiguredByアノテーションを外し、StoreConfigurationの型をCustomStoreConfigurationに変更するだけで
設定ファイルのCacheを使っている方のテストコードは動作します。

@Store
// @ConfiguredBy(SimpleMapCacheStoreConfiguration.class)  // ココ
public class SimpleMapCacheStore<K, V> implements ExternalStore<K, V> {
    Logger logger = Logger.getLogger(getClass());

    //SimpleMapCacheStoreConfiguration storeConfiguration;
    CustomStoreConfiguration storeConfiguration;  // ココ
    String storeName;

    Map<ByteBuffer, EntryData> store;

    InitializationContext ctx;

    @Override
    public void init(InitializationContext ctx) {
        this.ctx = ctx;
        storeConfiguration = ctx.getConfiguration();

こっちは、StoreConfigurationBuilderを直接使っているのでダメですが。

            Configuration configuration =
                    persistenceConfigurationBuilder
                            .addStore(new SimpleMapCacheStoreConfigurationBuilder(persistenceConfigurationBuilder))
                            .addProperty("storeName", "programmaticallyStore")
                            .build();

このような動作になるのは、@ConfiguredByアノテーションが指定されていない場合は、CustomStoreConfigurationBuilderおよび

CustomStoreConfigurationが使われるからです。

https://docs.jboss.org/infinispan/10.1/apidocs/org/infinispan/configuration/cache/CustomStoreConfiguration.html

https://docs.jboss.org/infinispan/10.1/apidocs/org/infinispan/configuration/cache/CustomStoreConfigurationBuilder.html

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/configuration/parsing/Parser.java#L2546-L2563

Interceptor

CacheStoreが呼ばれるようになるのは、Persistenceの設定を行うとInterceptorが追加されるからです。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L247-L280

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheLoaderInterceptor.java

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheWriterInterceptor.java

DataContainer側になかった場合に、CacheStoreからロードしようとするのは、このあたりの挙動ですね。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheLoaderInterceptor.java#L204-L206

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheLoaderInterceptor.java#L390-L450

繰り返しますが、ここでロードしたデータはDataContainer側に保持されるようになっています。

エントリの作成時間やlifespanなどについて

ところで、ログ出力時にcreatedやexpiryTime、lastUsedなどが軒並み-1になっていたと思います。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}

Metadata(lifespan、maxIdle)の部分に関しては、ここで設定されています。いずれも、なにも指定しない場合の初期値は-1です。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/cache/impl/CacheImpl.java#L195-L196

created、expiryTime、lastUsedなどは、expirationの設定を行うと設定されるようになります。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/container/impl/AbstractInternalDataContainer.java#L150

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/container/impl/InternalEntryFactoryImpl.java#L46-L61

たとえば、Cacheの設定にlifespanを入れると

        <distributed-cache name="declarativeSimpleStoreCache">
            <expiration lifespan="10000"/>
            <persistence>
                <store class="org.littlewings.infinispan.persistence.SimpleMapCacheStore">
                    <property name="storeName">declarativeStore</property>
                </store>
            </persistence>
        </distributed-cache>

値が入るようになります。

3月 26, 2020 12:26:14 午前 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-22348 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = 1585149974089, expiryTime = 1585149984089, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=10000, maxIdle=-1}

まとめ

InfinispanのCacheStoreまわりのAPIが知らないうちにいろいろ変わっていたので、まずはCacheLoader/CacheWriterと設定の基本まわりを
おさらいしてみました。

前に見た時よりも、だいぶ中も追えたのではないでしょうか?

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

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-cachestore