CLOVER🍀

That was when it all began.

Infinispan Server 7ことはじめ

前回は、EmbeddedなローカルモードでInfinispanを使ってみましたが、今回は単体でサーバとして動作するInfinispan Serverを使ってみます。

こちらのページの「Server」と書かれたボタンを選び、Infinispan Serverをダウンロードします。

http://infinispan.org/download/

自分が取得したファイルは、「infinispan-server-7.0.0.Final-bin.zip」でした。

Infinispan Serverとは?

前回はEmbeddedなキャッシュライブラリみたいな形で使いましたが、こちらはサーバとして独立して起動させる、外部プログラムからアクセス可能なタイプになります。

アクセス方法は、以下の4つがあります。

  • Hot Rod(Javaクライアント向け)
  • Memcached(テキストプロトコルのみのサポート)
  • REST
  • WebSocket(実験的サポート)

Javaから使うならHot Rod、他の言語から使うのならMemcachedかRESTといったところでしょう。WebSocketの方は、未だ触ったことがありません…。

Hot Rodについては、一応Java以外の言語向けのクライアントライブラリも存在するようです。

Hot Rod Clients
http://infinispan.org/hotrod-clients/

やっぱり試したことないですけど…。

インストール&起動

では、まずはインストールから。

ダウンロードしたZIPファイルを、適当なディレクトリに展開します。

$ unzip infinispan-server-7.0.0.Final-bin.zip

起動。

$ ./infinispan-server-7.0.0.Final/bin/standalone.sh

起動スクリプトはクラスタリングを意識したものもあるのですが、今回は単体でいきます。

起動中には、こんなログや

16:07:04,122 INFO  [org.wildfly.extension.undertow] (MSC service thread 1-3) JBAS017519: Undertow HTTP listener default listening on /127.0.0.1:8080

〜省略〜

16:07:10,323 INFO  [org.infinispan.server.endpoint] (MSC service thread 1-2) JDGS010002: REST mapped to /rest

こんなログが出力されるので

16:07:10,829 INFO  [org.infinispan.server.endpoint] (MSC service thread 1-2) JDGS010000: WebSocketServer starting
16:07:10,830 INFO  [org.infinispan.server.endpoint] (MSC service thread 1-2) JDGS010001: WebSocketServer listening on 127.0.0.1:8181
16:07:10,835 INFO  [org.infinispan.server.endpoint] (MSC service thread 1-4) JDGS010000: MemcachedServer starting
16:07:10,837 INFO  [org.infinispan.server.endpoint] (MSC service thread 1-4) JDGS010001: MemcachedServer listening on 127.0.0.1:11211
16:07:10,836 INFO  [org.infinispan.server.endpoint] (MSC service thread 1-1) JDGS010000: HotRodServer starting
16:07:10,845 INFO  [org.infinispan.server.endpoint] (MSC service thread 1-1) JDGS010001: HotRodServer listening on 127.0.0.1:11222

プロトコルとポートはなんとなくわかります。まあ、設定ファイルを見てもいいですけどね。

ちなみに、Infinispan Server 7.0.0.FinalはWildFly 8.1.0.Finalがベースになっているようです。

16:06:46,000 INFO  [org.jboss.modules] (main) JBoss Modules version 1.3.3.Final
16:06:47,200 INFO  [org.jboss.msc] (main) JBoss MSC version 1.2.2.Final
16:06:47,477 INFO  [org.jboss.as] (MSC service thread 1-1) JBAS015899: JBoss Infinispan Server 7.0.0.Final (WildFly 8.1.0.Final) starting

起動完了時。

16:07:12,365 INFO  [org.jboss.as] (Controller Boot Thread) JBAS015874: JBoss Infinispan Server 7.0.0.Final (WildFly 8.1.0.Final) started in 27983ms - Started 146 of 156 services (48 services are lazy, passive or on-demand)

では、このInfinispan Serverを使ってキャッシュにアクセスするプログラムを書いていきます。

準備

ここで作成するプログラムで必要な依存関係を含むbuild.sbtを、以下のように定義しました。
build.sbt

name := "remote-starter"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.4"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

incOptions := incOptions.value.withNameHashing(true)

fork in Test := true

parallelExecution in Test := false

libraryDependencies ++= Seq(
  // for Hot Rod
  "org.infinispan" % "infinispan-client-hotrod" % "7.0.0.Final",
  "net.jcip" % "jcip-annotations" % "1.0" % "provided",
  // for Memcached
  "net.spy" % "spymemcached" % "2.11.4",
  // for REST
  "net.databinder.dispatch" %% "dispatch-json4s-jackson" % "0.11.2",
  "org.scalatest" %% "scalatest" % "2.2.2" % "test"
)

また、テストコードの雛形は以下のように用意しておきます。使うクライアントによってimport文が変わるので、その点にはご注意を。
src/test/scala/org/littlewings/infinispan/remote/RemoteCacheSpec.scala

package org.littlewings.infinspan.remote

import java.util.concurrent.TimeUnit

// for Hot Rod Client
import org.infinispan.client.hotrod.RemoteCacheManager
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder

// for Memcached Client
import java.net.InetSocketAddress
import net.spy.memcached.MemcachedClient

// for REST Client
import dispatch._
import dispatch.Defaults._
import org.json4s._
import org.json4s.jackson.Serialization

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class RemoteCacheSpec extends FunSpec {
  describe("Infinispan RemoteCache Spec") {
    // ここに、テストを書く
  }
}

Hot Rod

では、まずはHot Rodから。

Java Hot Rod client
http://infinispan.org/docs/7.0.x/user_guide/user_guide.html#_java_hot_rod_client

いきなりですが、今回用にCache定義をServerに足してみます。以下のように定義されたcache-container-の定義ですが、
*infinispan-server-7.0.0.Final/standalone/configuration/standalone.xmlより抜粋

        <subsystem xmlns="urn:infinispan:server:core:7.0" default-cache-container="local">
            <cache-container name="local" default-cache="default" statistics="true">
                <local-cache name="default" start="EAGER">
                    <locking acquire-timeout="30000" concurrency-level="1000" striping="false"/>
                    <transaction mode="NONE"/>
                </local-cache>
                <local-cache name="memcachedCache" start="EAGER">
                    <locking acquire-timeout="30000" concurrency-level="1000" striping="false"/>
                    <transaction mode="NONE"/>
                </local-cache>
                <local-cache name="namedCache" start="EAGER"/>
            </cache-container>
            <cache-container name="security"/>
        </subsystem>

このようにします。

        <subsystem xmlns="urn:infinispan:server:core:7.0">
            <cache-container name="local" default-cache="default" statistics="true">
                <local-cache name="default" start="EAGER">
                    <locking striping="false" acquire-timeout="30000" concurrency-level="1000"/>
                    <transaction mode="NONE"/>
                </local-cache>
                <local-cache name="memcachedCache" start="EAGER">
                    <locking striping="false" acquire-timeout="30000" concurrency-level="1000"/>
                    <transaction mode="NONE"/>
                </local-cache>
                <local-cache name="namedCache" start="EAGER"/>
                <local-cache name="hotrodCache"/>
                <local-cache name="hotrodWithExpiredCache">
                    <expiration max-idle="3000" lifespan="5000"/>
                </local-cache>
            </cache-container>
            <cache-container name="security"/>
        </subsystem>

この2つを足しました。

                <local-cache name="hotrodCache"/>
                <local-cache name="hotrodWithExpiredCache">
                    <expiration max-idle="3000" lifespan="5000"/>
                </local-cache>

このCacheを使って、確認していきます。

まずは簡単に。

    it("Hot Rod Client") {
      val manager =
        new RemoteCacheManager(new ConfigurationBuilder()
          .addServer
          .host("localhost")
          .port(11222)
          .build)

      val cache = manager.getCache[String, String]("hotrodCache")

      cache.put("key1", "value1")
      cache.put("key2", "value2")

      cache.get("key1") should be ("value1")
      cache.get("key2") should be ("value2")
      cache.get("non-exists-key") should be (null)

      cache.remove("key1")
      cache.remove("key2")

      cache.get("key1") should be (null)
      cache.get("key2") should be (null)

      cache.stop()
      manager.stop()
    }

この範囲だと、ほとんどMap、もしくは組み込み時で使ったCacheと変わりません。

ちなみに、組み込み時と異なり、事前に定義されていない名前のCacheを取得しようとすると、nullになります。

    it("Hot Rod Client, using non exists Cache") {
      val manager =
        new RemoteCacheManager(new ConfigurationBuilder()
          .addServer
          .host("localhost")
          .port(11222)
          .build)

      manager.getCache[String, String]("nonExistsCache") should be (null)

      manager.stop()
    }

この時、コンソール上ではこんな感じにメッセージが出力され(例外が飛ぶわけではないです)

WARN: ISPN004005: Error received from the server: org.infinispan.server.hotrod.CacheNotFoundException: Cache with name 'nonExistsCache' not found amongst the configured caches

サーバ側でも例外が出力されます。

16:21:14,120 ERROR [org.infinispan.server.hotrod.HotRodDecoder] (HotRodServerWorker-13-3) ISPN005003: Exception reported: org.infinispan.server.hotrod.CacheNotFoundException: Cache with name 'nonExistsCache' not found amongst the configured caches

Embedded Cacheの時も同じだと思いますが、ちゃんとCacheの定義はしましょうね、と。

有効期限付きCacheの例。

    it("Hot Rod Client, with expiration") {
      val manager =
        new RemoteCacheManager(new ConfigurationBuilder()
        .addServer
        .host("localhost")
        .port(11222)
        .build)

      val cache = manager.getCache[String, String]("hotrodWithExpiredCache")

      cache.put("key1", "value1")
      cache.put("key2", "value2")

      TimeUnit.SECONDS.sleep(2)

      cache.get("key1") should be ("value1")

      TimeUnit.SECONDS.sleep(2)

      // maxIdle
      cache.get("key1") should be ("value1")
      cache.get("key2") should be (null)

      TimeUnit.SECONDS.sleep(2)

      // lifespan
      cache.get("key1") should be (null)
      cache.get("key2") should be (null)

      cache.stop()
      manager.stop()
    }

こちらも、Embeddedな場合とそう変わらず。

Memcached

続いて、Memcachedプロトコルでアクセスしてみます。ただし、Infinispan Serverがサポートしているのは、テキストプロトコルのみです。

Using Infinispan Memcached Server
http://infinispan.org/docs/7.0.x/user_guide/user_guide.html#_using_infinispan_memcached_server

Memcachedクライアントライブラリとしては、spymemcachedを使いました。

spymemcached
https://code.google.com/p/spymemcached/

API Document
http://www.couchbase.com/autodocs/

Memcachedの方は、Infinispan Serverに事前定義済みのものをそのまま使いました。

                <local-cache name="memcachedCache" start="EAGER">
                    <locking striping="false" acquire-timeout="30000" concurrency-level="1000"/>
                    <transaction mode="NONE"/>
                </local-cache>

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

    it("Memcached Client") {
      val memcachedClient = new MemcachedClient(new InetSocketAddress("localhost", 11211))

      memcachedClient.set("key1", 3, "value1")  // expire 3 sec
      memcachedClient.set("key2", 3, "value2")  // expire 3sec

      memcachedClient.get("key1") should be ("value1")
      memcachedClient.get("key2") should be ("value2")

      TimeUnit.SECONDS.sleep(3)

      memcachedClient.get("key1") should be (null)
      memcachedClient.get("key2") should be (null)
    }

REST

最後は、HTTPでアクセスできる、REST形式です。

Infinispan REST Server
http://infinispan.org/docs/7.0.x/user_guide/user_guide.html#_infinispan_rest_server

URLとしては、「http://[ホスト名]:[ポート番号]/rest/[Cache名]」を起点に、GETやPUT、DELETEでキャッシュ操作ができます。詳しくは、ドキュメントへ…。

REST用のCacheも、専用のものを追加しました。

                <local-cache name="restCache"/>

…実は、REST Serverとして使うパターンが1番ハマりました。

デフォルトでBASIC認証がかかっているので、認証するために以下のスクリプトでユーザを追加したのですが

$ ./infinispan-server-7.0.0.Final/bin/add-user.sh

こういうエラーが出てどうにもうまくいきませんでした…。

Caused by: java.lang.IllegalStateException: JBAS017329: No security context found
	at org.wildfly.extension.undertow.security.JAASIdentityManagerImpl.verifyCredential(JAASIdentityManagerImpl.java:115) [wildfly-undertow-8.1.0.Final.jar:8.1.0.Final]

というか、アレですね、Security Subsystemがわかってない。

かなり詰まったので、もういいやと思ってBASIC認証を外す方向に転換しました。以下の2箇所のXML定義を

            <rest-connector cache-container="local" auth-method="BASIC" security-domain="other" virtual-server="default-host"/>

            <http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>

このように変更。

            <rest-connector cache-container="local" virtual-server="default-host"/>

            <http-interface security-realm="ManagementRealm" http-upgrade-enabled="true">

ご参考)
管理CLIでのコマンド。

[standalone@localhost:9990 /] /subsystem=endpoint/rest-connector=rest-connector:undefine-attribute(name=security-domain)
[standalone@localhost:9990 /] /subsystem=endpoint/rest-connector=rest-connector:undefine-attribute(name=auth-method)
[standalone@localhost:9990 /] /subsystem=remoting/http-connector=http-remoting-connector:undefine-attribute(name=security-realm) 
[standalone@localhost:9990 /] reload

はい。

で、クライアントの方についてはなんでもいいのですが、今回はJSONでやることにしました。

使ったのは、Dispatchのjson4sjacksonモジュールを使いました。

Dispatch
http://dispatch.databinder.net/Dispatch.html

json4s
https://github.com/json4s/json4s

作ったコードは、こんな感じです。

Case Classを使ったので、トップレベルのクラスとして定義して

case class Pair(key: String, value: String)

テストコードはこのように。

    it("REST Client") {
      val http = Http()

      val baseUrl = host("localhost", 8080) / "rest" / "restCache"
      val baseReq =
        baseUrl
          .setContentType("application/json", "UTF-8")

      val p1 = Pair("key1", "value1")
      val p2 = Pair("key2", "value2")

      implicit val formats = Serialization.formats(NoTypeHints)

      // PUT
      val p1RegisterReq = (baseReq / p1.key).PUT << Serialization.write(p1)
      http(p1RegisterReq).apply().getStatusCode should be (200)

      val p2RegisterReq = (baseReq / p2.key).PUT << Serialization.write(p2)
      http(p2RegisterReq).apply().getStatusCode should be (200)

      // GET
      val p1GetReq = baseReq / p1.key
      val p1GetRes = http(p1GetReq OK as.json4s.Json)
      p1GetRes.apply().extract[Pair] should be (p1)

      val p2GetReq = baseReq / p2.key
      val p2GetRes = http(p2GetReq OK as.json4s.Json)
      p2GetRes.apply().extract[Pair] should be (p2)

      // DELETE
      val p1DeleteReq = (baseReq / p1.key).DELETE << Serialization.write(p1)
      http(p1DeleteReq).apply().getStatusCode should be (200)

      val p2DeleteReq = (baseReq / p2.key).DELETE << Serialization.write(p2)
      http(p2DeleteReq).apply().getStatusCode should be (200)

      // GET
      http(p1GetReq).apply().getStatusCode should be (404)
      http(p2GetReq).apply().getStatusCode should be (404)

      http.shutdown()
    }

今回は地味にDispatchの勉強になった気がします。

最終的に使いませんでしたが、

      val baseReq =
        baseUrl
          .as("username", "password")  // または .as_!
          .setContentType("application/json", "UTF-8")

でBASIC認証が使えることもわかりました…。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-starter