CLOVER🍀

That was when it all began.

Payara 4.1.153で追加された、NamedCacheを試す

先日、Payara 4.1.153がリリースされまして。

What's New in Payara Server 4.1.153 ?
http://www.payara.co.uk/whats-new-in-payara-server-41153

Payara Server 4.1.153 Release Notes
http://www.payara.co.uk/release_notes

新機能などの中で、個人的にちょっと気になった@NamedCacheを試してみます。「What's New in Payara Server 4.1.153 ?」内では、「JCache Injection」と書かれています。

では、ちょっと使ってみましょう。

今回もPayara Microを使うので、Payara Micro 4.1.153は以下からダウンロードしておきます。

Downloads
http://www.payara.co.uk/downloads

@NamedCacheとは?

こちらのIssueから追加されたもの、ってことなのかな。

CDI container should be able to inject Cache #188
https://github.com/payara/Payara/issues/188

CDIでCacheをインジェクションする際に、Cacheの見分けができない、どれをインジェクションしたらいいのかわからない、という状況を解決するために導入されたものってことでいいんでしょうか。

ドキュメントは、こちらに。

JCache (Payara 4.1.153) 3.2.1 Creating a custom Cache using Injection
https://github.com/payara/Payara/wiki/JCache-%28Payara-4.1.153%29#321-creating-a-custom-cache-using-injection

JCache (Payara 4.1.153) 4.1 NamedCache Annotation
https://github.com/payara/Payara/wiki/JCache-%28Payara-4.1.153%29#41-namedcache-annotation

パッと見た感じ、こういう使い方をしそうですね。

  • @Injectと一緒に、@NamedCacheを付与してCacheをインジェクション
  • インジェクションされるCacheは、@NamedCacheのcacheNameにより選択される
  • Cacheが未存在だった場合は、@NamedCacheで指定したcacheNameのCacheが作成される
  • @NamedCacheにはいくつかパラメーターがあり、生成されるCacheの設定が可能

このあたりの実装が行われているのが、このあたりです。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L82

@NamedCache自体は、こちら。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/impl/NamedCache.java#L34

いかんせんドキュメントがほぼこれだけなので、あとは自分で試しながら見ていきたいと思います。

@NamedCacheを使ったサンプルの実装

それでは、@NamedCache(fish.payara.cdi.jsr107.impl.NamedCache)を使ったサンプルを書いていきます。

まずは、Mavenの設定。
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>payara-micro-namedcache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

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

    <build>
        <finalName>payara-micro-namedcache</finalName>
    </build>

    <dependencies>
        <dependency>
            <groupId>fish.payara.extras</groupId>
            <artifactId>payara-micro</artifactId>
            <version>4.1.153</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

今回は、JAX-RSを使って試します。
src/main/java/org/littlewings/hazelcast/JaxrsApplication.java

package org.littlewings.hazelcast;

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

@ApplicationPath("rest")
public class JaxrsApplication extends Application {
}

JAX-RSリソースクラス。
src/main/java/org/littlewings/hazelcast/CalcResource.java

package org.littlewings.hazelcast;

import java.util.concurrent.TimeUnit;
import javax.cache.Cache;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import fish.payara.cdi.jsr107.impl.NamedCache;

@Path("calc")
@RequestScoped
public class CalcResource {
    @NamedCache(cacheName = "myCache")
    @Inject
    private Cache myCache;

    @GET
    @Path("add")
    @Produces(MediaType.TEXT_PLAIN)
    public String add(@QueryParam("a") int a, @QueryParam("b") int b) throws Exception {
        String key = a + "+" + b;

        if (myCache.containsKey(key)) {
            return String.format("CacheName = %s, result = %d", myCache.getName(), myCache.get(key));
        } else {
            TimeUnit.SECONDS.sleep(3);

            int result = a + b;
            myCache.put(key, result);

            return String.format("CacheName = %s, result = %d", myCache.getName(), result);
        }
    }
}

「myCache」というCacheをインジェクションしています。

    @NamedCache(cacheName = "myCache")
    @Inject
    private Cache myCache;

また、レスポンスにCacheの名前を含めるようにしているので、

            return String.format("CacheName = %s, result = %d", myCache.getName(), myCache.get(key));

これらでも確認することにします。

            return String.format("CacheName = %s, result = %d", myCache.getName(), result);

パッケージングして

$ mvn package

デプロイ。

$ java -jar payara-micro-4.1.153.jar --deploy target/payara-micro-namedcache.war

動作確認。

## 1回目
$ time curl 'http://localhost:8080/payara-micro-namedcache/rest/calc/add?a=3&b=5'
CacheName = myCache, result = 8
real	0m3.524s
user	0m0.008s
sys	0m0.000s

## 2回目
$ time curl 'http://localhost:8080/payara-micro-namedcache/rest/calc/add?a=3&b=5'
CacheName = myCache, result = 8
real	0m0.023s
user	0m0.003s
sys	0m0.003s

Cacheの名前も「myCache」になっていますし、OKそうですね。

とここまでだと簡単そうに見えるのですが…。

ハマったこと

簡単に使えるかな?と思い、テキトーにやったらけっこうハマりました…。

Cacheに型パラメーターを適用できない

最初、何も考えずにこういう定義をしていたら

    @NamedCache(cacheName = "myCache")
    @Inject
    private Cache<String, Integer> myCache;

デプロイに失敗しました。

[2015-08-01T17:06:16.313+0900] [Payara 4.1] [SEVERE] [NCLS-CORE-00026] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1438416376313] [levelValue: 1000] [[
  Exception during lifecycle processing
org.glassfish.deployment.common.DeploymentException: CDI deployment failure:WELD-001408: Unsatisfied dependencies for type Cache<String, Integer> with qualifiers @Default
  at injection point [BackedAnnotatedField] @NamedCache @Inject private org.littlewings.hazelcast.CalcResource.myCache
  at org.littlewings.hazelcast.CalcResource.myCache(CalcResource.java:0)

このCacheにインジェクションできませんよ、と…。

で、ドキュメントをよくよく見ると、

@NamedCache(cacheName = "custom")
@Inject
Cache cache;

あー、raw typeですねー。

そもそも、JSR107Producer#createCacheの戻り値がraw typeのCacheでございます。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L77

というわけで、Cacheに型パラメーターは適用するな、と。

@NamedCacheのパラメーターとして、keyClassとvalueClassが指定できるのですが…それほど意味がなかったり…。

有効期限を設定できない

@NamedCacheでは、JCacheで使うCacheLoader、CacheWriter、ExpiryPolicyを使うためのファクトリクラスを設定できるようです。

今回は、有効期限を設定しようとexpiryPolicyFactoryClassを使おうと考えました。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/impl/NamedCache.java#L94

が、Classクラスがあるだけなので、何を設定していいのかがわかりません(笑)。

JSR107Producer#createCacheのソースを見ていて…
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L102

ExpiryPolicyを実装したクラスを作成すればいいのかなーと思い、実装。
src/main/java/org/littlewings/hazelcast/MyExpiryPolicy.java

package org.littlewings.hazelcast;

import java.util.concurrent.TimeUnit;
import javax.cache.expiry.Duration;
import javax.cache.expiry.ExpiryPolicy;

public class MyExpiryPolicy implements ExpiryPolicy {
    @Override
    public Duration getExpiryForCreation() {
        return new Duration(TimeUnit.SECONDS, 10);
    }

    @Override
    public Duration getExpiryForAccess() {
        return null;
    }

    @Override
    public Duration getExpiryForUpdate() {
        return null;
    }
}

意味的には、エントリ登録後、10秒で有効期限切れするということになります。

では、これを使ってJAX-RSリソースクラスを実装。
src/main/java/org/littlewings/hazelcast/ExpiryCalcResource.java

package org.littlewings.hazelcast;

import java.util.concurrent.TimeUnit;
import javax.cache.Cache;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.ExpiryPolicy;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import fish.payara.cdi.jsr107.impl.NamedCache;

@Path("expirycalc")
@RequestScoped
public class ExpiryCalcResource {
    @NamedCache(cacheName = "expiryCache", expiryPolicyFactoryClass = MyExpiryPolicy.class)
    @Inject
    private Cache expiryCache;

    @GET
    @Path("add")
    @Produces(MediaType.TEXT_PLAIN)
    public String add(@QueryParam("a") int a, @QueryParam("b") int b) throws Exception {
        String key = a + "+" + b;

        if (expiryCache.containsKey(key)) {
            return String.format("CacheName = %s, result = %d", expiryCache.getName(), expiryCache.get(key));
        } else {
            TimeUnit.SECONDS.sleep(3);

            int result = a + b;
            expiryCache.put(key, result);

            return String.format("CacheName = %s, result = %d", expiryCache.getName(), result);
        }
    }
}

では、デプロイして動作確認。

$ time curl 'http://localhost:8080/payara-micro-namedcache/rest/expirycalc/add?a=3&b=5'
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><title>Payara Micro #badassfish - Error report</title><style type="text/css"><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 500 - Internal Server Error</h1><hr/><p><b>type</b> Exception report</p><p><b>message</b>Internal Server Error</p><p><b>description</b>The server encountered an internal error that prevented it from fulfilling this request.</p><p><b>exception</b> <pre>javax.servlet.ServletException: java.lang.RuntimeException: Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy</pre></p><p><b>root cause</b> <pre>java.lang.RuntimeException: Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy</pre></p><p><b>root cause</b> <pre>java.lang.ClassNotFoundException: org.littlewings.hazelcast.MyExpiryPolicy</pre></p><p><b>note</b> <u>The full stack traces of the exception and its root causes are available in the Payara Micro #badassfish logs.</u></p><hr/><h3>Payara Micro #badassfish</h3></body></html>
real	0m0.622s
user	0m0.002s
sys	0m0.005s

がっつりエラー…。

裏を見ると、Hazelcastがコケております。

[2015-08-01T17:15:53.606+0900] [Payara 4.1] [SEVERE] [] [com.hazelcast.cache.impl.operation.CacheContainsKeyOperation] [tid: _ThreadID=47 _ThreadName=hz.c30e3817-4f56-44ee-99cc-0368703760b2.partition-operation.thread-3] [timeMillis: 1438416953606] [levelValue: 1000] [[
  [172.17.42.1]:5900 [dev] [3.5] Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy
java.lang.RuntimeException: Failed to create an instance of org.littlewings.hazelcast.MyExpiryPolicy
	at javax.cache.configuration.FactoryBuilder$ClassFactory.create(FactoryBuilder.java:134)
	at com.hazelcast.cache.impl.AbstractCacheRecordStore.<init>(AbstractCacheRecordStore.java:132)
	at com.hazelcast.cache.impl.CacheRecordStore.<init>(CacheRecordStore.java:61)
	at com.hazelcast.cache.impl.CacheService.createNewRecordStore(CacheService.java:77)
	at com.hazelcast.cache.impl.CachePartitionSegment$1.createNew(CachePartitionSegment.java:56)
	at com.hazelcast.cache.impl.CachePartitionSegment$1.createNew(CachePartitionSegment.java:53)
	at com.hazelcast.util.ConcurrencyUtil.getOrPutSynchronized(ConcurrencyUtil.java:40)
	at com.hazelcast.cache.impl.CachePartitionSegment.getOrCreateCache(CachePartitionSegment.java:76)
	at com.hazelcast.cache.impl.AbstractCacheService.getOrCreateCache(AbstractCacheService.java:120)
	at com.hazelcast.cache.impl.operation.AbstractCacheOperation.beforeRun(AbstractCacheOperation.java:68)
	at com.hazelcast.spi.impl.operationservice.impl.OperationRunnerImpl.run(OperationRunnerImpl.java:131)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.processOperation(OperationThread.java:154)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.process(OperationThread.java:110)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.doRun(OperationThread.java:101)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.run(OperationThread.java:76)
Caused by: java.lang.ClassNotFoundException: org.littlewings.hazelcast.MyExpiryPolicy
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at javax.cache.configuration.FactoryBuilder$ClassFactory.create(FactoryBuilder.java:130)
	... 14 more
]]

しかも、ClassNotFoundException…。

Caused by: java.lang.ClassNotFoundException: org.littlewings.hazelcast.MyExpiryPolicy

よく見ると、これはHazelcast内部の動作なんですよねー。しかも、アプリケーションとは別スレッド。

	at com.hazelcast.spi.impl.operationservice.impl.OperationRunnerImpl.run(OperationRunnerImpl.java:131)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.processOperation(OperationThread.java:154)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.process(OperationThread.java:110)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.doRun(OperationThread.java:101)
	at com.hazelcast.spi.impl.operationexecutor.classic.OperationThread.run(OperationThread.java:76)

ここで、WARファイルでデプロイしたクラスまでのClassLoaderの階層は、以下の様になっているようです。

org.glassfish.web.loader.WebappClassLoader
org.glassfish.internal.api.DelegatingClassLoader
java.net.URLClassLoader
com.sun.enterprise.v3.server.APIClassLoaderServiceImpl$APIClassLoader
sun.misc.Launcher$ExtClassLoader

つまり、今回作成した自前のExpiryPolicyはWebappClassLoaderでロードされていることになります。

これに対して、HazelcastのOperationThreadが動作している時のClassLoaderの階層は…

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader

いたって普通。Hazelcastをロードしているの、Webアプリじゃないですからねぇ…。しかもリクエストを処理しているスレッドとは別スレッド…なんか詰んでいる感じが…。

※javax.cache.configuration.FactoryBuilder$ClassFactoryも、ContextClassLoaderは使うのですが
https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/configuration/FactoryBuilder.java#L128

とすると、CacheLoader、CacheWriterを使っても同じ目に遭いそうな気がします。

微妙…。

一応補足しておくと、@Injectを契機に動作するJSR107Producer#createCacheによるCacheの作成は成功します。その後の、Hazelcastの内部動作がついてこれない、と。

最初にうまく動いていたこちらのコードでは、特にWebアプリケーション内にあるClassを要求するわけではないので、うまくいくという話ですね。

    @NamedCache(cacheName = "myCache")
    @Inject
    private Cache myCache;

こちらのコードでも、OperationThreadの動作自体は行われます。

なお、蛇足ですが…Payara MicroのJARに先ほどの自作のExpiryPolicyのClassを入れれば動作はしますが…

$ jar uvf payara-micro-4.1.153.jar -C target/payara-micro-namedcache/WEB-INF/classes org/littlengs/hazelcast/MyExpiryPolicy.class
org/littlewings/hazelcast/MyExpiryPolicy.classを追加中です(=822)(=425)(48%収縮されました)

こんなこと、まずやりませんね!!

というわけで、忘れます。なお、今回はPayara Microで動かしていますが、Payara Serverを使って確認しても、やっぱりClassNotFoundExceptionになりました…。

Interceptorには無関係

今回追加された@NamedCacheは、あくまで@Injectに関わるものなので、CDIと連携するためのInterceptorの機能とは無関係です。

仮に、こういうクラスを書いたとしても
src/main/java/org/littlewings/hazelcast/CachedCalcResource.java

package org.littlewings.hazelcast;

import java.util.concurrent.TimeUnit;
import javax.cache.annotation.CacheResult;
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

@Path("cachedcalc")
@RequestScoped
public class CachedCalcResource {
    @GET
    @Path("add")
    @Produces(MediaType.TEXT_PLAIN)
    @CacheResult(cacheName = "calcCache")
    public int add(@QueryParam("a") int a, @QueryParam("b") int b) throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);

        return a + b;
    }
}

Cacheの設定を行う術がありません。

InterceptorまわりのCacheの取得方法は、依然としてCacheManager#createCacheおよびgetCacheだからです。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/impl/PayaraCacheResolverFactory.java#L49

この場合、EJBのSingleon+StarupやWebListenerなどで最初にCacheの定義を行う必要があるでしょう。

まとめ

Payara 4.1.153で追加された、@NamedCacheアノテーションを試してみました。

ちょっと、実際にこれがいろんなところで使われる状況は想像できませんが…。

Cacheって、インスタンスそのものをいろんなクラスに@Injectするものとは考えにくいですし、それがraw typeのままでもね…とちょっと思います。それに、(ClassLoaderの話は無視しても)Cacheの設定が可能とはいえ、同じ@NamedCacheを指定するものについては、全部のアノテーションの定義を揃えないと意味ないですし。

@NamedCacheに指定したパラメーターを使ってCacheを作成するのは、最初の1回だけなので…。
https://github.com/payara/Payara/blob/payara-server-4.1.153/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107/JSR107Producer.java#L94

Interceptorには効果がないことを考えると、Cacheの設定を行いたい場合には、やっぱりアプリケーションの初期化時になんとかしてしまうのかなーと思いました。

@NamedCacheを使って、Cacheの名前解決を行うことはできますが(ただし、raw type…)、@Qualifier書いてもいいのかなーとも思いました。

なんか、微妙な結論に…。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/payara-micro-namedcache