CLOVER🍀

That was when it all began.

Hibernate SearchとSpringのCache機能で、InfinispanのCacheを共有する

前に1度挫折したネタを、別のアプローチでやってみようということで。

以前、Spring BootとHibernate Search、そしてInfinispanを使って、こんなエントリを書きました。

Spring Boot×Hibernate Searchで、インデックスを複数Nodeで共有する
http://d.hatena.ne.jp/Kazuhira/20141223/1419347497

この方法では、Hibernate SearchのLuceneのインデックスの保存先をInfinispanとして、その設定方法はクラスパス上の設定ファイルとしました。ただ、これだとInfinispanのインスタンスHibernate Searchに握られてしまうので、他の用途に使えません。

で、JNDIリソースとして定義しようとしたのがこちら。

組み込みTomcat(on Spring Boot)でJNDIリソースを扱う
http://d.hatena.ne.jp/Kazuhira/20141227/1419709045

でまあ、失敗したわけですよ。組み込みTomcatにJNDIリソースとしてねじ込んでも、アプリケーションの起動時にHibernate Searchから見えなくて。

その後、もうちょっとHibernate Searchの実装を見ていると、意外とできるんじゃないか?という気がしまして、再度チャレンジということでやってみました。

今回は、Hibernate Searchで定義したInfinispanのCacheManagerを、SpringのCache機能でも共有するようにしてみます。

なお、このエントリを書くにあたっての参考情報は、こちらです。

SpringのCache Abstractionについて
https://blog.ik.am/#/entries/339

Cache Abstraction
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html

Using Infinispan as a Spring Cache provider
http://infinispan.org/docs/7.2.x/user_guide/user_guide.html#_using_infinispan_as_a_spring_cache_provider

準備

用意したpomはこちら。Spring BootでSpring MVCと、Spring Data JPAを使います。
pom.xml

<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>hibernate-search-infinispan-cache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 後で -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

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

個々の依存関係は、別に説明します。

データベースはMySQLとするので、JDBCドライバを追加。

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.35</version>
            <scope>runtime</scope>
        </dependency>

Hibernate Searchの利用とインデックスの保存先はInfinispanということで、これらの依存関係を追加。Lucene Kuromojiは蛇足な気がします…。

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.hibernate</groupId>
                    <artifactId>hibernate-core</artifactId>
                </exclusion>
            </exclusions>
            <version>5.2.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-infinispan</artifactId>
            <version>5.2.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-kuromoji</artifactId>
            <version>4.10.4</version>
        </dependency>

Hibernate Searchが見ているHibernate ORM/EntityManagerが微妙にSpring Boot JPAより新しいらしく、そのままだと動かなかったのでHibernate SearchからHibernateを除外しました。

SpringのCacheとしてInfinispanを使うために、Spring 4用のモジュールを追加。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring4</artifactId>
            <version>7.2.0.Final</version>
        </dependency>

最後に、主題と関係ありませんが、Lombok

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.4</version>
            <scope>provided</scope>
        </dependency>

依存関係の定義は、こんな感じです。

テーブル定義

JPAで使うテーブルの定義は、こんな感じ。

CREATE TABLE article(
    id INT AUTO_INCREMENT,
    contents VARCHAR(255),
    PRIMARY KEY(id)
);

主キー以外は、1カラムだけ。とにかく、簡単にいきます。

設定ファイル

Spring Bootで使う設定ファイルは、このように定義しました。
src/main/resources/application.yml

spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=utf-8&characterSetResults=utf-8&useServerPrepStmts=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false
    username: kazuhira
    password: password
  jpa:
    hibernate.ddl-auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        search:
          default:
            directory_provider: infinispan
          infinispan.configuration_resourcename: infinispan.xml
          analyzer: org.apache.lucene.analysis.ja.JapaneseAnalyzer
          lucene_version: LUCENE_4_10_4

Infinispanの設定ファイルは、「infinispan.xml」として定義。

で、Infinispanの設定ファイルは、このように用意。
src/main/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:7.2 http://www.infinispan.org/schemas/infinispan-config-7.2.xsd"
        xmlns="urn:infinispan:config:7.2">
    <jgroups>
        <stack-file name="udp" path="jgroups.xml"/>
    </jgroups>

    <cache-container name="indexingCacheManager" shutdown-hook="DONT_REGISTER">
        <transport cluster="cluster" stack="udp"/>

        <distributed-cache name="springCache">
            <expiration lifespan="10000" max-idle="-1"/>
        </distributed-cache>

        <distributed-cache name="LuceneIndexesData" mode="SYNC" remote-timeout="25000">
            <transaction mode="NONE"/>
            <state-transfer enabled="true" timeout="480000" await-initial-transfer="true"/>
            <indexing index="NONE"/>
            <locking striping="false" acquire-timeout="10000" concurrency-level="500" write-skew="false"/>
            <eviction max-entries="-1" strategy="NONE"/>
            <expiration max-idle="-1"/>
        </distributed-cache>

        <replicated-cache name="LuceneIndexesMetadata" mode="SYNC" remote-timeout="25000">
            <transaction mode="NONE"/>
            <state-transfer enabled="true" timeout="480000" await-initial-transfer="true"/>
            <indexing index="NONE"/>
            <locking striping="false" acquire-timeout="10000" concurrency-level="500" write-skew="false"/>
            <eviction max-entries="-1" strategy="NONE"/>
            <expiration max-idle="-1"/>
        </replicated-cache>

        <replicated-cache name="LuceneIndexesLocking" mode="SYNC" remote-timeout="25000">
            <transaction mode="NONE"/>
            <state-transfer enabled="true" timeout="480000" await-initial-transfer="true"/>
            <indexing index="NONE"/>
            <locking striping="false" acquire-timeout="10000" concurrency-level="500" write-skew="false"/>
            <eviction max-entries="-1" strategy="NONE"/>
            <expiration max-idle="-1"/>
        </replicated-cache>
    </cache-container>

</infinispan>

「springCache」というのは、SpringのCache機能で使うために定義したものです。

        <distributed-cache name="springCache">
            <expiration lifespan="10000" max-idle="-1"/>
        </distributed-cache>

10秒間のTTLを設定しています。

その他のCacheは、すべてインデックスの保存等のLucene関係で使われます。

クラスタ構成ができるように定義しているので、JGroupsの設定も入れておきます。
src/main/resources/jgroups.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="urn:org:jgroups"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/JGroups-3.6.xsd">
    <UDP
            mcast_addr="${jgroups.udp.mcast_addr:228.6.7.8}"
            mcast_port="${jgroups.udp.mcast_port:46655}"
            ucast_recv_buf_size="150k"
            ucast_send_buf_size="130k"
            mcast_recv_buf_size="150k"
            mcast_send_buf_size="130k"
            ip_ttl="${jgroups.ip_ttl:2}"
            thread_naming_pattern="pl"
            enable_diagnostics="false"

            thread_pool.min_threads="${jgroups.thread_pool.min_threads:2}"
            thread_pool.max_threads="${jgroups.thread_pool.max_threads:30}"
            thread_pool.keep_alive_time="60000"
            thread_pool.queue_enabled="false"

            internal_thread_pool.min_threads="${jgroups.internal_thread_pool.min_threads:5}"
            internal_thread_pool.max_threads="${jgroups.internal_thread_pool.max_threads:20}"
            internal_thread_pool.keep_alive_time="60000"
            internal_thread_pool.queue_enabled="true"
            internal_thread_pool.queue_max_size="500"

            oob_thread_pool.min_threads="${jgroups.oob_thread_pool.min_threads:20}"
            oob_thread_pool.max_threads="${jgroups.oob_thread_pool.max_threads:200}"
            oob_thread_pool.keep_alive_time="60000"
            oob_thread_pool.queue_enabled="false"
            />

    <PING/>
    <MERGE3 min_interval="10000"
            max_interval="30000"
            />
    <FD_SOCK/>
    <FD_ALL timeout="60000"
            interval="15000"
            timeout_check_interval="5000"
            />
    <VERIFY_SUSPECT timeout="5000"
            />

    <pbcast.NAKACK2 xmit_interval="1000"
                    xmit_table_num_rows="50"
                    xmit_table_msgs_per_row="1024"
                    xmit_table_max_compaction_time="30000"
                    max_msg_batch_size="100"
                    resend_last_seqno="true"
            />

    <UNICAST3 xmit_interval="500"
              xmit_table_num_rows="50"
              xmit_table_msgs_per_row="1024"
              xmit_table_max_compaction_time="30000"
              max_msg_batch_size="100"
              conn_expiry_timeout="0"
            />
    <pbcast.STABLE stability_delay="500"
                   desired_avg_gossip="5000"
                   max_bytes="1M"
            />
    <pbcast.GMS print_local_addr="true"
                join_timeout="15000"
            />

    <!-- <tom.TOA/> -->
    <!-- the TOA is only needed for total order transactions-->

    <UFC max_credits="2m"
         min_threshold="0.40"
            />
    <MFC max_credits="2m"
         min_threshold="0.40"
            />
    <FRAG2/>
</config>

設定ファイル関係は、ここまで。

Spring Data JPAHibernate Searchを使ったサンプル

では、アプリケーションを書いていきます。

まずは、Spring Data JPAHibernate Searchを使ってサンプルの部分。

JPAのEntityは、このように定義。
src/main/java/org/littlewings/spring/domain/Article.java

package org.littlewings.spring.domain;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;

@Data
@NoArgsConstructor
@Entity
@Table(name = "article")
@Indexed
public class Article implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    @Field
    private String contents;

    public Article(String contents) {
        this.contents = contents;
    }
}

Lombokを使って、短く。

RestController。Controllerと言いながらEntityManagerなどをゴリゴリと使いますが、Repositoryとか書いていくとちょっと収まらなくなるので…。
src/main/java/org/littlewings/spring/web/SearchController.java

package org.littlewings.spring.web;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;

import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.BooleanJunction;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.littlewings.spring.domain.Article;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("search")
@Transactional
public class SearchController {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private EntityManager entityManager;

    @RequestMapping(value = "create", method = RequestMethod.PUT)
    public Article create(@RequestParam String contents) {
        Article article = new Article(contents);
        entityManager.persist(article);
        return article;
    }

    @RequestMapping(value = "get/{id}", method = RequestMethod.GET)
    public Article get(@PathVariable Long id) {
        return entityManager.find(Article.class, id);
    }

    @RequestMapping(value = "search", method = RequestMethod.GET)
    @SuppressWarnings("unchecked")
    public List<Article> search(@RequestParam String keyword) {
        FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();

        QueryBuilder queryBuilder =
                fullTextEntityManager
                        .getSearchFactory()
                        .buildQueryBuilder()
                        .forEntity(Article.class)
                        .get();

        List<org.apache.lucene.search.Query> queries =
                Stream
                        .of(keyword.split("\\s+"))
                        .map(k -> queryBuilder.keyword().onField("contents").matching(k).createQuery())
                        .collect(Collectors.toList());

        BooleanJunction<BooleanJunction> booleanJunction = queryBuilder.bool();
        queries.stream().forEach(q -> booleanJunction.must(q));
        org.apache.lucene.search.Query luceneQuery = booleanJunction.createQuery();

        logger.info("Lucene Query => {}", luceneQuery);

        return (List<Article>) fullTextEntityManager
                .createFullTextQuery(luceneQuery, Article.class)
                .setSort(new Sort(new SortField("id", SortField.Type.LONG)))
                .getResultList();
    }

    private FullTextEntityManager getFullTextEntityManager() {
        return Search.getFullTextEntityManager(entityManager);
    }

    /*
    @PostConstruct
    public void reindexing() throws InterruptedException {
        logger.info("indexing...");
        getFullTextEntityManager().createIndexer().startAndWait();
        logger.info("done.");
    }
    */
}

最後のこの部分は、単一Nodeであればアプリケーション起動時に再インデキシングしてくれるのですが、クラスタ構成にするとちょっとうまくいかなくなるので、コメントアウトしています。

    @PostConstruct
    public void reindexing() throws InterruptedException {
        logger.info("indexing...");
        getFullTextEntityManager().createIndexer().startAndWait();
        logger.info("done.");
    }

SpringのCache機能を使う部分

続いては、SpringのCache機能を使ったクラスを書きます。

SpringのCacheのアノテーションを使って、効果を確認するためのServiceクラス。
src/main/java/org/littlewings/spring/service/CalcService.java

package org.littlewings.spring.service;

import java.util.concurrent.TimeUnit;

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@CacheConfig(cacheNames = "springCache")
public class CalcService {
    @Cacheable
    public int add(int a, int b) {
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
        }

        return a + b;
    }
}

そして、Cacheのアノテーションを使ったクラスを使う、RestController。
src/main/java/org/littlewings/spring/web/CacheController.java

package org.littlewings.spring.web;

import org.littlewings.spring.service.CalcService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("cache")
public class CacheController {
    @Autowired
    private CalcService calcService;

    @RequestMapping(value = "add", method = RequestMethod.GET)
    public Integer add(@RequestParam int a, @RequestParam int b) {
        return calcService.add(a, b);
    }
}

ここまでは、割と単純です。

JavaConfig

で、今回苦労したのがJavaConfigです。最終的に、こんな定義になりました。
src/main/java/org/littlewings/spring/config/Config.java

package org.littlewings.spring.config;

import javax.persistence.EntityManager;

import org.hibernate.search.FullTextSession;
import org.hibernate.search.infinispan.spi.CacheManagerService;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.spi.SearchIntegrator;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.spring.provider.SpringEmbeddedCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

@Configuration
@EnableCaching
public class Config {
    @Autowired
    private EntityManager entityManager;

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Bean
    public EmbeddedCacheManager embeddedCacheManager() {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);

        return transactionTemplate.execute(status -> {
            status.setRollbackOnly();

            FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
            FullTextSession session = fullTextEntityManager.unwrap(FullTextSession.class);
            SearchIntegrator integrator = session.getSearchFactory().unwrap(SearchIntegrator.class);
            CacheManagerService cacheManagerService =
                    integrator.getServiceManager().requestService(CacheManagerService.class);
            return cacheManagerService.getEmbeddedCacheManager();
        });
    }

    @Bean
    public CacheManager cacheManager() {
        return new SpringEmbeddedCacheManager(embeddedCacheManager());
    }
}

Hibernate Searchが管理しているInfinispanのCacheManagerを引っこ抜くには、こんなコードを書けばいいのかなーと実装を見ていて思いまして。

    @Bean
    public EmbeddedCacheManager embeddedCacheManager() {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);

        return transactionTemplate.execute(status -> {
            status.setRollbackOnly();

            FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
            FullTextSession session = fullTextEntityManager.unwrap(FullTextSession.class);
            SearchIntegrator integrator = session.getSearchFactory().unwrap(SearchIntegrator.class);
            CacheManagerService cacheManagerService =
                    integrator.getServiceManager().requestService(CacheManagerService.class);
            return cacheManagerService.getEmbeddedCacheManager();
        });
    }

ただ、ロールバックするように設定しているとはいえ、トランザクションを使っているところが微妙ですね…。

最初、こんな感じで書いていたら

    @Bean
    public EmbeddedCacheManager embeddedCacheManager() {
        FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
        FullTextSession session = fullTextEntityManager.unwrap(FullTextSession.class);
        SearchIntegrator integrator = session.getSearchFactory().unwrap(SearchIntegrator.class);
        CacheManagerService cacheManagerService =
                integrator.getServiceManager().requestService(CacheManagerService.class);
        return cacheManagerService.getEmbeddedCacheManager();
    }

トランザクションがないよと怒られまして。

Caused by: java.lang.IllegalStateException: No transactional EntityManager available
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:267)

EntityManagerからトランザクションを開始するのもNGらしく、SpringかJTAを使えという感じだったので、今回はTransactionTemplateを使って実装しました。

Transaction Management
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html

エントリポイント

最後は、アプリケーションのエントリポイント。
src/main/java/org/littlewings/spring/App.java

package org.littlewings.spring;

import org.littlewings.spring.config.Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(Config.class)
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

こちらは、簡単に。

動作確認

それでは、動かしてみましょう。

パッケージング。

$ mvn package

2つNodeを起動してみます。

## Node 1
$ java -jar target/hibernate-search-infinispan-cache-0.0.1-SNAPSHOT.jar

## Node 2
$ java -jar target/hibernate-search-infinispan-cache-0.0.1-SNAPSHOT.jar --server.port=8180

Node 2のHTTPリッスンポートは、8180とします。

起動すれば、まずは成功…。前にやった時は、起動することもできませんでした…。

2つNodeを起動すると、途中のログでクラスタが構成されたことがわかります。

2015-05-30 23:15:25.619  INFO 128580 --- [ceneIndexesData] o.i.r.t.jgroups.JGroupsTransport         : ISPN000094: Received new cluster view for channel cluster: [xxxxx-27108|1] (2) [xxxxx-27108, xxxxx-61235]
データ登録とHibernate Searchによる検索

それでは、データ登録や検索をしてみます。

$ time curl -XPUT 'http://localhost:8080/search/create?contents=Java+Programming+Language.'
{"id":3,"contents":"Java Programming Language."}
real	0m1.427s
user	0m0.007s
sys	0m0.000s
$ time curl -XPUT 'http://localhost:8080/search/create?contents=Apache+Lucene+is+full-text+search+engine.'
{"id":4,"contents":"Apache Lucene is full-text search engine."}
real	0m0.215s
user	0m0.008s
sys	0m0.000s
$ time curl -XPUT 'http://localhost:8080/search/create?contents=Spring+Boot.'
{"id":5,"contents":"Spring Boot."}
real	0m0.160s
user	0m0.007s
sys	0m0.000s

3つほど登録。

Node 2に対して、検索。

$ time curl 'http://localhost:8180/search/search?keyword=apache+lucene'
[{"id":4,"contents":"Apache Lucene is full-text search engine."}]
real	0m1.034s
user	0m0.008s
sys	0m0.004s

Node 1に対しても動くので、うまくインデックスが共有できているようです。

$ time curl 'http://localhost:8080/search/search?keyword=java'
[{"id":3,"contents":"Java Programming Language."}]
real	0m0.228s
user	0m0.004s
sys	0m0.004s
Cacheの確認

最後に、Cache機能を試してみます。

Node 1に対して、アクセス。

$ time curl 'http://localhost:8080/cache/add?a=3&b=5'
8
real	0m3.054s
user	0m0.009s
sys	0m0.000s

sleepが入っているので、3秒かかります。

この後、すぐにNode 2に対してアクセスすると、結果がすぐに得られます。

$ time curl 'http://localhost:8180/cache/add?a=3&b=5'
8
real	0m0.063s
user	0m0.007s
sys	0m0.004s

OKそうですね。

また、このCacheは10秒で有効期限切れするので、しばらく放っておくとまた時間がかかるようになります。Node 2へアクセス。

$ time curl 'http://localhost:8180/cache/add?a=3&b=5'
8
real	0m3.075s
user	0m0.006s
sys	0m0.004s

続いて、Node 1へ。

$ time curl 'http://localhost:8080/cache/add?a=3&b=5'
8
real	0m0.016s
user	0m0.004s
sys	0m0.004s

こちらにも反映されているようです。

Cacheのキーですが、デフォルトはメソッドの引数すべてのようなので、今回の実装方法だとリクエストのパラメータを変えると、それぞれ別のCacheエントリになります。

$ time curl 'http://localhost:8080/cache/add?a=3&b=5'
8
real	0m3.039s
user	0m0.007s
sys	0m0.005s
$ time curl 'http://localhost:8080/cache/add?a=8&b=2'
10
real	0m3.031s
user	0m0.009s
sys	0m0.000s

とりあえず、やりたいことは実現できた感じです。