Apache Geode - お手軽にサーバーサイドでロジックを実行する方法

#はじめに
分散システム型 KVS 実装の一つである Apache Geode にて、サーバーサイドで何かロジックを実行したいときには Function Execution という機能を使うのが一般的です。ただ、 Functoin Execution は高機能であるため、設定項目や実ロジック以外のコーディング量が多くなりがちで、特定ロジックをクラスター内のデータ配置とか関係なくいずれかのキャッシュサーバーノードで一度だけ実行して返り値も要求しない、といった単純なケースに使うのはやや気が引けるかもしれません。

本記事では、Apache Geode にて、Function Execution 機能を使わずにもっとお手軽にサーバーサイドでロジックを実行する方法を紹介します。以下、任意のロジックを実行する実体を「コマンド」と呼称することにします。なお、本記事の方法は Apache Geode 1.6.0 にて動作確認済みです。

#目的
本記事では、単純なサーバーサイドでのロジック実行例として、以下の内容を Function Execution 機能を使わずに設定・実装することとします。

- 指定したリージョンのデータ全削除コマンド（つまり、``Regoin#clear`` の実行）を、クラスター内のいずれかのキャッシュサーバーノードで一度だけ実行、返り値なし

#仕組み
以下のような仕組みで、Function Execution 機能を使わずにサーバーサイドでロジックを実行します。

1. ロジックを実行するために専用のリージョンを用意（以下、Command リージョン） - データ格納しないのでプロキシーリージョンで良い（``refid=REPLICATED_PROXY``）
2. Command リージョンに、コマンドオブジェクトをバリューとして ``put`` - キーは使わないので任意の値
3. サーバーサイドで事前に Command リージョンに付与した ``CacheWriter`` で、バリューとして渡されたコマンドを実行

ここで、一般的によく使われる ``CacheListner`` ではなく ``CacheWriter`` を使う理由は、前者は基本的に付与されたキャッシュサーバー全てが反応してしまってコマンドが重複実行される可能性があるのに対して、後者は付与されたキャッシュサーバーのいずれか一つで実行されるからです。

参考までに、以下実行イメージ図を掲載します。
![20180926-executionImage.png](https://qiita-image-store.s3.amazonaws.com/0/133966/362f2ed6-3feb-da42-dd7e-21e31219c83e.png)

#コマンドの作成
方針としてはコマンド毎に個別にクラスを用意することを前提に、``CacheWriter`` でコマンドを実行するロジックを共通化するために、以下のようなコマンドインターフェースを定義します。

```java:Command.java
public interface Command {
  public void process();
}
```

次に、これを実装する形で、指定したリージョンのデータ削除コマンドクラスを以下のように記述します。このコマンドクラスをインスタンス化する際に指定するリージョン（``regionName``）を元に、コマンドインターフェースで定義した ``process`` メソッドで ``Region#clear`` を実行します。

```java:ClearRegionCommand.java
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.Region;

import java.io.Serializable;

public class ClearRegionCommand implements Command, Serializable {
  private String regionName;
  
  public ClearRegionCommand(String regionName) {
    this.regionName = regionName;
  }
  
  public void process() {
    Region region = CacheFactory.getAnyInstance().getRegion(this.regionName);
    if (region != null) {
      region.clear();
    }
  }
}
```

なお、コマンドオブジェクト自体がネットワーク経由でサーバーに送信されるので、シリアライズ可能なオブジェクトとして実装する必要があります。ここでは、``java.io.Serializable`` を実装して済ませていますが、Apache Geode ということでオブジェクトシリアライズ技術として PDX を使ってもいいですね。

そして、コマンドを実行する ``CacheWriter`` の実装です。``beforeCreate`` メソッド内で、``put`` により発生するイベントから、コマンドオブジェクトを取得して ``process`` メソッドを実行するという内容です。ちなみに、``CacheWriter`` は多くの場合キャッシュ設定ファイル（以下、cache.xml）で定義することを考えて、念のため ``Declarable`` を実装しておきます。

```java:ProcessCommandCacheWriter.java
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheWriterException;
import org.apache.geode.cache.Declarable;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.util.CacheWriterAdapter;

import java.util.Properties;

public class ProcessCommandCacheWriter extends CacheWriterAdapter<Integer,Command> implements Declarable {
  public void beforeCreate(EntryEvent<Integer,Command> event) throws CacheWriterException {
    Command command = event.getNewValue();
    command.process();
  }

  public void initialize(Cache cache, Properties properties) { }

  public void close() {}
}
```

#``CacheWriter`` の設定
ここでは、cache.xml を使って設定します。``refid`` に ``REPLICATE_PROXY`` を設定した Command リージョンに付与する形となります。Example リージョンは、今回実装したコマンドにより ``Regoin#clear`` 実行対象となるサンプルリージョンとなります。

```xml:cache.xml
<?xml version="1.0" encoding="UTF-8"?>
<cache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://geode.apache.org/schema/cache"
       xsi:schemaLocation="http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd"
       version="1.0" lock-lease="120" lock-timeout="60" search-timeout="300" is-server="false" copy-on-read="false">

    <cache-server port="0" />

    <region name="Example" refid="REPLICATE" />

    <region name="Command" refid="REPLICATE_PROXY">
        <region-attributes>
            <cache-writer>
                <class-name>ProcessCommandCacheWriter</class-name>
            </cache-writer>
        </region-attributes>
    </region>
</cache>
```

#CLASSPATH の設定
クライアントから投入されたコマンドは、サーバーで実行されるので、サーバーの CLASSPATH に各種関連クラスへのパスを設定する必要があります。コマンドクラス本体（ここでは、``ClearRegionCommand``）は必要に応じて ``gfsh deploy`` コマンドで動的に設定可能ですが、サーバー起動時には最低限以下のクラスへのパスを設定する必要があります。

- ``CacheWriter`` クラス（ここでは、``ProcessCommandCacheWriter`` クラス）
- コマンドインターフェース（ここでは、Command インターフェース）

インターフェースは、``gfsh deploy`` コマンドでは CLASSPATH 設定できないようです

#クライアントからコマンド投入
以下のように、コマンドオブジェクトを作成して Command リージョンにバリューとして ``put`` するようなコーディングをします。 

```java
ClientCache cache = new ClientCacheFactory()
  .set("cache-xml-file", "client-cache.xml")
  .create();
cache.getRegion("Command").put(0, new ClearRegionCommand("Example"));
```

参考までに、以下、クライアントサイドのサンプル cache.xml を掲載します。

``` xml:client-cache.xml
<?xml version="1.0" encoding="UTF-8"?>
<client-cache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://geode.apache.org/schema/cache"
       xsi:schemaLocation="http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd"
       version="1.0" copy-on-read="false">

    <pool name="MyPool" subscription-enabled="true">
        <locator host="xxx.xxx.xxx.xxx" port="xxxxx" />
    </pool>

    <region name="Example">
        <region-attributes pool-name="MyPool" refid="CACHING_PROXY" />
    </region>

    <region name="Command">
        <region-attributes pool-name="MyPool" refid="PROXY" />
    </region>
</client-cache>
```

#最後に
記事としてまとめてみると、Function Executoin の場合同様、思ったよりもコードとか設定内容が多い気がしますが、クライアントからのコマンド投入が一行で済むのは利用者にとっては、お手軽かな、と思っています。

さらに、今回ご紹介した方法ですと、Command リージョンに Gateway Sender の設定を追加することで、WAN 経由で、Gateway Receiver を設定した別クラスターでも任意のロジック実行という離れ業も可能です。ご興味と需要があれば試してみてください。