CLOVER🍀

That was when it all began.

Spring SessionのInfinispan向けコードを書いてみた

Spring Session 1.1.0で、Hazelcastのサポートが追加されました。

Redirecting…

Spring Session 1.1.0 Released

コードは、このあたり。
https://github.com/spring-projects/spring-session/tree/1.2.0.RELEASE/spring-session/src/main/java/org/springframework/session/hazelcast

このHazelcastのサポートまわりのコードを眺めていて、これなら他のものでもやれそうでは?と思い、Spring SessionのInfinispan版を書いてみました。

https://github.com/kazuhira-r/spring-session-infinispan

まあ、言ってしまうとほぼHazelcastでのコードをInfinispanに書き換えたものなので、まんまと言えばまんまです。

Embedded ModeとClient/Server Mode(Hot Rod)の両方を書いていて、テストコードも(ほぼHazelcast向けコードまんまですが)書いています。一部、Spring Sessionのテストコードから拝借したものも。

有効化のための各アノテーションの定義はこんな感じで、セッションを保持するCacheの名前はとりあえず「springSessions」にしておきました。

Embedded向け。
src/main/java/org/littlewings/spring/session/infinispan/embedded/config/annotation/web/http/EnableInfinispanEmbeddedHttpSession.java

package org.littlewings.spring.session.infinispan.embedded.config.annotation.web.http;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(InfinispanEmbeddedHttpSessionConfiguration.class)
@Configuration
public @interface EnableInfinispanEmbeddedHttpSession {
    int maxInactiveIntervalInSeconds() default 1800;

    String sessionsCacheName() default "springSessions";
}

Client/Server(Hot Rod)向け。
src/main/java/org/littlewings/spring/session/infinispan/remote/config/annotation/web/http/EnableInfinispanRemoteHttpSession.java

package org.littlewings.spring.session.infinispan.remote.config.annotation.web.http;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(InfinispanRemoteHttpSessionConfiguration.class)
@Configuration
public @interface EnableInfinispanRemoteHttpSession {
    int maxInactiveIntervalInSeconds() default 1800;

    String sessionsCacheName() default "springSessions";
}

使う時には、Embedded Modeならinfinispan-coreを、Client/Server Modeなら「infinispan-client-hotrod」を足す感じで。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>8.2.2.Final</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>8.2.2.Final</version>
        </dependency>

Embedded Modeを使うサンプルはこんな感じで、EmbeddedCacheManagerをBean定義します。
※HttpSessionを扱ってるところはテキトーですが

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

    @RequestMapping("session")
    public Map<String, String> session(HttpSession session) {
        LocalDateTime now = (LocalDateTime) session.getAttribute("now");
        if (now == null) {
            now = LocalDateTime.now();
            session.setAttribute("now", now);
        }

        Map<String, String> response = new HashMap<>();
        response.put("isNew", Boolean.toString(session.isNew()));
        response.put("now", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        return response;
    }

    @Bean
    public EmbeddedCacheManager embeddedCacheManager() {
        EmbeddedCacheManager cacheManager =
                new DefaultCacheManager(new GlobalConfigurationBuilder().transport().defaultTransport().build());
        cacheManager
                .defineConfiguration("springSessions",
                        new org.infinispan.configuration.cache.ConfigurationBuilder().clustering().cacheMode(CacheMode.DIST_SYNC).build());
        return cacheManager;
    }
}

Client/Server Modeならこんな感じ。RemoteCacheManagerをBean定義します。Client/Server Modeの場合は、あらかじめServer側にCacheを定義しておく必要がありますが…。

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

    @RequestMapping("session")
    public Map<String, String> session(HttpSession session) {
        LocalDateTime now = (LocalDateTime) session.getAttribute("now");
        if (now == null) {
            now = LocalDateTime.now();
            session.setAttribute("now", now);
        }

        Map<String, String> response = new HashMap<>();
        response.put("isNew", Boolean.toString(session.isNew()));
        response.put("now", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        return response;
    }

    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new org.infinispan.client.hotrod.configuration.ConfigurationBuilder()
                        .addServers("localhost:11222")
                        .build());
    }
}

作ってみて

Spring SessionにMapSessionRepositoryというものがあって、こちらを使うとMapベースのものならけっこう簡単に作れるんだなーということがわかりました。
https://github.com/spring-projects/spring-session/blob/1.2.0.RELEASE/spring-session/src/main/java/org/springframework/session/MapSessionRepository.java

まあ、ひととおりプログラム書いて、テストコード書いて、サンプル書いて試してみたところで、初めてSpring Sessionを使ったコードを書いたことにふと気付きましたが…。

気になったこと、悩んだこと

ExpiredEvent発火のタイミング

Infinispanのexpirationのデフォルト設定だと、wake-up-intervalが1分になっていて、expireこそするもののExpiredEventがすぐに発火せずにテストコードではこの値を縮める必要がありました…。

Cluster Listener

Client/Server ModeとEmbedded Mode(Distributed Cache)で、イベントを受け取るNodeの数が違うことがあるんじゃないかなぁと。最初は対称性を取ろうとして、Embedded Modeの場合はCluster Listenerにして全Nodeがイベントを受け取るようにした方がいいんじゃないかな?と思ったのですが、どうなのでしょう。

Spring Sessionの他の実装を見ていると、Client/Server Modeのものかそうでないものか(Distributed/Partitionedなものを使っている場合)で、イベントを受け取るNode数が変わるような気も…。

今回は、通常のListenerにしてDistributed Cacheの時はエントリのOwnerのNodeがイベントを受け取るようになっています。

Eviction

EvictedEventの扱いは、悩みどころでした。Persistenceを考えると、Evictされる=Removeされるは必ずしも成立しないですし。ただ、Client/Server ModeのListenerはEvictedEventをサポートしていないので、今回はEmbedded Modeの場合も外しておきました。

なお、HazelcastのListenerがExpireとEvictをそう区別していないことに、ここで気付きましたが…。

とりあえず、最低ラインは作れたので満足しました。あとは他のSpring Sessionの実装を眺めて、気ままにいじっていこうかなと思います。

InfinispanはWildFlyのセッション(クラスタ可)で使われていますが、他のグリッドとかと違ってServletFilterとかでの実装は提供してないんですよね。なので、ちょっと作ってみたいなぁとは思っていましたが、ServletFilterまで頑張る気にはちょっとなれず。そこにSpring Sessionがあったのでやってみました的な。

まあ、今回は興味本位です。