CLOVER🍀

That was when it all began.

InfinispanのTree APIを使ってみる

ちょっと脇道系でしょうか?InfinispanのTree APIが気になっていたので、遊んでみました。

Tree API Module
https://docs.jboss.org/author/display/ISPN/Tree+API+Module

けっこう面白そうな機能なのですが、予想外のところでハマったりしましたけど…。

Tree APIとは?

データを階層化して格納するためのAPIで、Fqn(Fully Qualified Name)で表現されたパスを使用します。例えば、

/this/is/a/fqn/path
/another/path

みたいな感じで。特別なパスとして、rootもあり

/

で表現されます。

Infinispanの前身であるJBoss Cacheで提供していた機能に似たようなAPIがあったようで、移行しやすくしてますよ〜みたいなことを、ドキュメントでは謳っています。

はい。

で、Fqnで定義されたパスは、Nodeとして表現されます。そして、Nodeにはキーと値のペア(要はMapです)を保存することができます。

ここ、最初ちょっとわかりにくいのですが…。例えば、NoSQLの分類をFqnで表して、キーをプロダクト名、値をURLみたいな感じで表現すると、こんな感じでしょうか。あと、RDBMSもつけてみましょう。

パス(Fqn)=Node キー
/NoSQL/カラムデータベース Cassandra http://cassandra.apache.org/
HBase http://hbase.apache.org/
/NoSQL/ドキュメントデータベース MongoDB http://www.mongodb.org/
/RDBMS MySQL http://dev.mysql.com/
PostgreSQL http://www.postgresql.org/

で、この表は最後に考えたので、今から載せるサンプルとはちょっと違いますが…まあ、APIを使ってみます。

Tree APIを使ってみる

最初にビルドの用意ですが、Tree APIはsbtを使うとクラスファイルがうまく解釈できなかったので、今回はGradleを使いました。
build.gradle

apply plugin: 'java'
apply plugin: 'application'

mainClassName = 'InfinispanTreeExample'

repositories {
    mavenCentral()

    maven {
        url 'https://repository.jboss.org/nexus/content/groups/public'
    }
}

dependencies {
    compile 'org.infinispan:infinispan-tree:5.3.0.CR1'
}

Tree APIは、InfinispanCoreモジュールではないので、明示的に指定する必要があります。

言語も、Javaにしておきました。…そういえば、InfinispanをJavaから使うのは初めてですね。

Tree APIを使うためには、設定をする必要があります。以下は、「treeCache」と名付けたCacheに対して、設定を行っている例です。

  <namedCache name="treeCache">
    <invocationBatching enabled="true" />
    <transaction
        transactionManagerLookupClass="org.infinispan.transaction.lookup.GenericTransactionManagerLookup"
        transactionMode="TRANSACTIONAL" />
  </namedCache>

って、Batching APIを有効化しているだけですが。これを有効化しないと、Tree APIは使用することができません。

では、Javaコードの方に移ります。以降のJavaコードでは、以下のimport文が入っているものとします。

import java.io.IOException;
import java.io.Serializable;

import org.infinispan.Cache;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.tree.Fqn;
import org.infinispan.tree.Node;
import org.infinispan.tree.TreeCacheFactory;
import org.infinispan.tree.TreeCache;

まあ、Serializableはなくてもいいですが…。

Tree APIの使うためには、まずはTreeCacheを作成する必要があります。

        EmbeddedCacheManager manager = null;
        Cache<String, Entry> cache = null;

        try {
            manager = new DefaultCacheManager("infinispan.xml");
            cache = manager.getCache("treeCache");

            TreeCacheFactory treeCacheFactory = new TreeCacheFactory();
            TreeCache<String, Entry> treeCache = treeCacheFactory.createTreeCache(cache);

Cacheを作る箇所まではいつも通りですが、その後でTreeCacheFactoryのインスタンスを生成して、そこからTreeCacheクラスを取得します。Cacheの名前を「treeCache」にしたのは、ちょっと紛らわしかったかも…。

TreeCacheには、登録するキーと値を型パラメータとして指定します。今回は、値としてそれ用のクラスを用意しました。

class Entry implements Serializable {
    public static final long serialVersionUID = 1L;

    private String name;

    public Entry(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return String.format("This is [%s]", name);
    }
}

まあ、大したことないものですが…。

では、まずはFqnを作成し、データを登録してみます。TreeCache#putで、Fqnとキー、そして値を指定します。

Fqn dataGridFqn = Fqn.fromString("/nosql/dataGrid");
treeCache.put(dataGridFqn, "infinispan", new Entry("Infinispan"));
treeCache.put(dataGridFqn, "gridGain", new Entry("GridGain"));

Fqnは、Fqnクラスのstaticメソッドから作成します。今回はfromStringメソッドを使用していますが、他にもfromElementsなどがあります。この例では、キー「infinispan」と「gridGain」に対して値を登録しています。

他にも、StringでいきなりFqnを指定してデータを登録することもできます。

treeCache.put("/nosql/dataGrid", "hazelcast", new Entry("Hazelcast"));

登録した情報を、ちょっと取得してみましょう。TreeCacheから取得します。

System.out.printf("get => %s%n",
                  treeCache.get(dataGridFqn, "infinispan"));  // V
System.out.printf("getData => %s%n",
                  treeCache.getData(dataGridFqn));  // Map<K, V>
System.out.printf("getKeys => %s%n",
                  treeCache.getKeys(dataGridFqn));  // Set<K>
System.out.printf("getNode => %s%n",
                  treeCache.getNode(dataGridFqn));  // Node<K, V>
System.out.printf("getNode#get => %s%n",
                  treeCache.getNode(dataGridFqn).get("infinispan")); // V

結果を、以下に載せます。

get => This is [Infinispan]
getData => {infinispan=This is [Infinispan], hazelcast=This is [Hazelcast], gridGain=This is [GridGain]}
getKeys => [infinispan, hazelcast, gridGain]
getNode => NodeImpl{fqn=/nosql/dataGrid}
getNode#get => This is [Infinispan]

コメントにも書いていますが、だいたいこんな対応付けです。

メソッド名 戻り値
get(Fqn, K) Fqnで指定したNodeから、指定のキーに対応する値を返却
getData(Fqn) Fqnで指定したNodeに登録してあるMapを返却
getNode(Fqn) Fqnで指定した、Node自身を返却
getKeys(Fqn) Fqnで指定したNodeから、登録してあるキーの集合を返却

最後の例は、getNode#getなのでNodeを特定した後、Nodeに対して指定のキーに対応する値を取得しています。

今度は、Fqn.fromElementsでFqnを作成してみます。

Fqn documentFqn = Fqn.fromElements("nosql", "documentDatabase");
treeCache.put(documentFqn, "mongoDB", new Entry("MongoDB"));
treeCache.put(documentFqn, "couchDB", new Entry("CouchDB"));

TreeCacheからrootのNodeが取得できるので、ここからNodeに対して子を追加していくこともできます。

Node<String, Entry> rootNode = treeCache.getRoot();
Node<String, Entry> columnDatabaseNode =
    rootNode
    .addChild(Fqn.fromElements("nosql"))
    .addChild(Fqn.fromElements("columnDatabase"));
columnDatabaseNode.put("cassandra", new Entry("Apache Cassandra"));
columnDatabaseNode.put("hbase", new Entry("Apache HBase"));

Node#addChildで指定しているFqnは、相対パス的な扱いになっていますね。

というわけで、Fqn.fromRelativeFqnから特定のFqnからの相対パスでFqnを作成することもできます。

Fqn nosqlFqn = Fqn.fromElements("nosql");
Fqn kvsFqn = Fqn.fromRelativeFqn(nosqlFqn, Fqn.fromElements("kvs"));
treeCache.put(kvsFqn, "redis", new Entry("Redis"));

Nodeを削除するには、TreeCache#removeNodeで。

treeCache.removeNode(Fqn.fromString("/nosql/documentDatabase"));

NodeからgetChildしていって、Node#removeChildで子を消すこともできます。

treeCache.getRoot().getChild(Fqn.fromString("nosql")).removeChild(Fqn.fromString("columnDatabase"));

なお、TreeCache#removeNodeではFqnを深い階層していして削除することができますが、Node#removeNodeでは自分の直下のNodeしか削除できないようです。つまり、こういう指定は無効です。

treeCache.getRoot().removeChild(Fqn.fromElements("nosql", "columnDatabase"));

特定のNodeに登録されているキーを指定して、対応するキーと値のペアを削除できます。

treeCache.getNode(Fqn.fromElements("nosql", "dataGrid")).remove("hazelcast");

Node#clearDateで、Nodeが持つ全データを削除。

treeCache.getNode(Fqn.fromElements("nosql", "dataGrid")).clearData();

TreeCache#moveで、Nodeの付け替えも可能です。

Node<String, Entry> graphDatabaseNode
    = treeCache.getRoot().addChild(Fqn.fromElements("other", "graphDatabase"));
graphDatabaseNode.put("neo4j", new Entry("Neo4j"));

treeCache.move(Fqn.fromString("/other/graphDatabase"), Fqn.fromString("/nosql"));

ここでは、「/other/graphDatabase」をFqnとして登録したパスを、「/nosql/graphDatabase」に移動しています。注意点は、第2引数は「新しい親となるFqnを指定する」ため、Linuxのmvコマンドみたいな感覚で使えるわけではありません。つまり、「リネームはできない」ということです。

Node#getChildrenで、Nodeの直接の子NodeのListを取得することもできます。

treeCache.getRoot().getChild(Fqn.fromElements("nosql")).getChildren();

Nodeは先ほど見たようにtoStringするとFqnが見えますが、そのNodeから下のFqnまで文字列化して表現するわけではありません。あくまで、自分自身までです。

ロックについて

Treeを操作している時には、ロックが発生するケースがあるようです。

Locking In Tree API
https://docs.jboss.org/author/display/ISPN/Tree+API+Module#TreeAPIModule-LockingInTreeAPI

  • Nodeに対してputすると、そのNodeにWriteロックを取得する。この場合、親NodeにWriteロックはかからないし、子Nodeにもロックはかからない
  • Nodeを追加したり削除した場合、親NodeにWriteロックはかからない
  • Nodeを移動する時は、そのNodeと子Nodeをロックする。移動先の新しいNodeの位置とその子Node(移動対象Nodeの子だと思います)にもロックがかかる

Nodeの移動の時だけは、本人以外にも親と子のNodeにロックがかかるということなのでしょうか…。

ところで、最初はsbt+Scalaでやろうとしていたのですが、こんなエラーが出てしまって進めなかったので、ここは諦めました…。

[error] error while loading Fqn, class file '/xxxxx/.ivy2/cache/org.infinispan/infinispan-tree/bundles/infinispan-tree-5.3.0.CR1.jar(org/infinispan/tree/Fqn.class)' is broken
[error] (class java.lang.RuntimeException/bad constant pool index: 0 at pos: 8277)

ちょっと前のバージョンのInfinispanにしても、ダメでした。

Tree APIって、ここ1〜2年くらい更新されていないみたいですし、Infinispan Moduleの中ではけっこう微妙な存在なのでしょうか?

最後、書いたコード全体です。

import java.io.IOException;
import java.io.Serializable;

import org.infinispan.Cache;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.tree.Fqn;
import org.infinispan.tree.Node;
import org.infinispan.tree.TreeCacheFactory;
import org.infinispan.tree.TreeCache;

public class InfinispanTreeExample {
    public static void main(String[] args) {
        EmbeddedCacheManager manager = null;
        Cache<String, Entry> cache = null;

        try {
            manager = new DefaultCacheManager("infinispan.xml");
            cache = manager.getCache("treeCache");

            TreeCacheFactory treeCacheFactory = new TreeCacheFactory();
            TreeCache<String, Entry> treeCache = treeCacheFactory.createTreeCache(cache);

            Fqn dataGridFqn = Fqn.fromString("/nosql/dataGrid");
            treeCache.put(dataGridFqn, "infinispan", new Entry("Infinispan"));
            treeCache.put(dataGridFqn, "gridGain", new Entry("GridGain"));

            treeCache.put("/nosql/dataGrid", "hazelcast", new Entry("Hazelcast"));

            System.out.printf("get => %s%n",
                              treeCache.get(dataGridFqn, "infinispan"));  // V
            System.out.printf("getData => %s%n",
                              treeCache.getData(dataGridFqn));  // Map<K, V>
            System.out.printf("getKeys => %s%n",
                              treeCache.getKeys(dataGridFqn));  // Set<K>
            System.out.printf("getNode => %s%n",
                              treeCache.getNode(dataGridFqn));  // Node<K, V>
            System.out.printf("getNode#get => %s%n",
                              treeCache.getNode(dataGridFqn).get("infinispan")); // V

            System.out.printf("getRoot => %s%n", treeCache.getRoot());

            Fqn documentFqn = Fqn.fromElements("nosql", "documentDatabase");
            treeCache.put(documentFqn, "mongoDB", new Entry("MongoDB"));
            treeCache.put(documentFqn, "couchDB", new Entry("CouchDB"));

            Node<String, Entry> rootNode = treeCache.getRoot();
            Node<String, Entry> columnDatabaseNode =
                rootNode
                .addChild(Fqn.fromElements("nosql"))
                .addChild(Fqn.fromElements("columnDatabase"));
            columnDatabaseNode.put("cassandra", new Entry("Apache Cassandra"));
            columnDatabaseNode.put("hbase", new Entry("Apache HBase"));

            Fqn nosqlFqn = Fqn.fromElements("nosql");
            Fqn kvsFqn = Fqn.fromRelativeFqn(nosqlFqn, Fqn.fromElements("kvs"));
            treeCache.put(kvsFqn, "redis", new Entry("Redis"));

            System.out.printf("get => %s%n", treeCache.get(kvsFqn, "redis"));

            System.out.printf("getChildren => %s%n",
                              treeCache.getRoot().getChild(Fqn.fromElements("nosql")).getChildren());

            treeCache.removeNode(Fqn.fromString("/nosql/documentDatabase"));
            //treeCache.removeNode(Fqn.fromElements("nosql", "documentDatabase"));

            treeCache.getRoot().getChild(Fqn.fromString("nosql")).removeChild(Fqn.fromString("columnDatabase"));
            //treeCache.getRoot().removeChild(Fqn.fromElements("nosql", "columnDatabase"));

            treeCache.getNode(Fqn.fromElements("nosql", "dataGrid")).remove("hazelcast");
            System.out.printf("getData => %s%n",
                              treeCache.getData(dataGridFqn));  // Map<K, V>

            treeCache.getNode(Fqn.fromElements("nosql", "dataGrid")).clearData();
            System.out.printf("getData => %s%n",
                              treeCache.getData(dataGridFqn));  // Map<K, V>

            Node<String, Entry> graphDatabaseNode
                = treeCache.getRoot().addChild(Fqn.fromElements("other", "graphDatabase"));
            graphDatabaseNode.put("neo4j", new Entry("Neo4j"));

            treeCache.move(Fqn.fromString("/other/graphDatabase"), Fqn.fromString("/nosql"));

            System.out.printf("getChildren => %s%n",
                              treeCache.getRoot().getChild(Fqn.fromElements("nosql")).getChildren());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (cache != null) {
                cache.stop();
            }

            if (manager != null) {
                manager.stop();
            }
        }
    }
}

class Entry implements Serializable {
    public static final long serialVersionUID = 1L;

    private String name;

    public Entry(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return String.format("This is [%s]", name);
    }
}