Spring session on Redis - what is the failover when Redis is down
Asked Answered
S

2

11

I am using Spring and Spring Security and want to use spring-session-data-redis with RedisHttpSessionConfiguration to enable storing session IDs on redis (so clients wont loose their sessions when webapp fails and switched over to another server).

My question, what happens when Redis server is down? Will spring be able to continue to work by storing session in memory until Redis is back up? Is there a way to configure this as so?

I am using Redis on AWS ElastiCache, and Failover can take several minutes before replacement primary node is configured on the DNS.

Stoa answered 29/10, 2015 at 14:9 Comment(2)
So the answer is no. If Redis goes down then spring-sesion-data-redis fails and throws an exception. Does anyone know of an implementation that doesn't? with perhaps backup data to in memory map?Stoa
I was thinking the same here. Any news on this, please update.Nich
F
2

I've managed to implement a fail-over mechanism to an in-memory session whenever Redis is unreachable. Unfortunately this can't be done just by a Spring property, so you have to implement your custom SessionRepository and configuring it to use the SessionRepositoryFilter which will fail-over to the in-memory cache whenever Redis is unreachable .

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;

@Component("customSessionRepository")
@Primary
public class CustomFailoverToMapSessionRepository implements SessionRepository {
    private static final Logger LOGGER = LoggerFactory.getLogger(CustomFailoverToMapSessionRepository.class);

    private GuavaBasedSessionRepository guavaBasedSessionRepository;
    private SessionRepository sessionRepository;

    public CustomFailoverToMapSessionRepository(SessionRepository sessionRepository, GuavaBasedSessionRepository guavaBasedSessionRepository) {
        this.sessionRepository = sessionRepository;
        this.guavaBasedSessionRepository = guavaBasedSessionRepository;
    }

    @Override
    public Session createSession() {
        Session session = null;
        MapSession mapSession = guavaBasedSessionRepository.createSession();
        try {
            session = sessionRepository.createSession();
            mapSession = toMapSession(session);
        } catch (Exception e) {
            LOGGER.warn("Unexpected exception when trying to create a session will create just an in memory session", e);
        }


        return session == null ? mapSession : session;
    }

    @Override
    public void save(Session session) {

        try {
            if (!isOfMapSession(session)) {
                sessionRepository.save(session);
            }
        } catch (Exception e) {
            LOGGER.warn("Unexpected exception when trying to save a session with id {} will create just an in memory session", session.getId(), e);
        }
        guavaBasedSessionRepository.save(toMapSession(session));

    }

    @Override
    public Session findById(String id) {

        try {
            return sessionRepository.findById(id);
        } catch (Exception e) {
            LOGGER.warn("Unexpected exception when trying to lookup a session with id {}", id, e);
            return guavaBasedSessionRepository.findById(id);
        }

    }

    @Override
    public void deleteById(String id) {
        try {
            try {
                guavaBasedSessionRepository.deleteById(id);
            } catch (Exception e) {
                //ignored
            }
            sessionRepository.deleteById(id);
        } catch (Exception e) {
            LOGGER.warn("Unexpected exception when trying to delete a session with id {}", id, e);

        }
    }

    private boolean isOfMapSession(Session session) {
        return session instanceof MapSession;
    }


    private MapSession toMapSession(Session session) {
        final MapSession mapSession = guavaBasedSessionRepository.createSession();
        if (session != null) {
            mapSession.setId(session.getId());
            mapSession.setCreationTime(session.getCreationTime());
            mapSession.setLastAccessedTime(session.getLastAccessedTime());
            mapSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
            session.getAttributeNames()
                    .forEach(attributeName -> mapSession.setAttribute(attributeName, session.getAttribute(attributeName)));
        }


        return mapSession;
    }

Implement the in-memory cache session repository using Guava

import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

@Component("guavaBasedSessionRepository")
public class GuavaBasedSessionRepository implements SessionRepository<MapSession> {

    private Cache<String, Session> sessionCache;

    @Value("${session.local.guava.cache.maximum.size}")
    private int maximumCacheSize;

    @Value("${redis.session.keys.timeout}")
    private long sessionTimeout;

    @PostConstruct
    void init(){
        sessionCache = CacheBuilder
                .newBuilder()
                .maximumSize(maximumCacheSize)
                .expireAfterWrite(sessionTimeout, TimeUnit.MINUTES)
                .build();
    }

    @Override
    public void save(MapSession session) {
        if (!session.getId().equals(session.getOriginalId())) {
            this.sessionCache.invalidate(session.getOriginalId());
        }
        this.sessionCache.put(session.getId(), new MapSession(session));
    }

    @Override
    public MapSession findById(String id) {
        Session saved = null;
        try {
            saved = this.sessionCache.getIfPresent(id);
        } catch (Exception e){
            //ignored
        }
        if (saved == null) {
            return null;
        }
        if (saved.isExpired()) {
            deleteById(saved.getId());
            return null;
        }
        return new MapSession(saved);
    }

    @Override
    public void deleteById(String id) {
        this.sessionCache.invalidate(id);
    }

    @Override
    public MapSession createSession() {
        MapSession result = new MapSession();
        result.setMaxInactiveInterval(Duration.ofSeconds(sessionTimeout));
        return result;
    }
 

Configure Spring to use the custom SessionRepository

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.session.Session;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieHttpSessionIdResolver;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.SessionRepositoryFilter;

import javax.annotation.PostConstruct;

@EnableRedisHttpSession
@Configuration
public class CustomSessionConfig {

    private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();

    @Autowired
    private CookieSerializer cookieSerializer;

    @PostConstruct
    public void init(){
        this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
    }

    @Bean
    @Primary
    public <S extends Session> SessionRepositoryFilter<? extends Session> sessionRepositoryFilter(CustomFailoverToMapSessionRepository customSessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(customSessionRepository);
        sessionRepositoryFilter.setHttpSessionIdResolver(this.defaultHttpSessionIdResolver);
        return sessionRepositoryFilter;
    }
Fleur answered 9/9, 2022 at 10:35 Comment(0)
L
1

As far as I can see, you will need to provide an implementation of CacheErrorHandler ( javadoc).

You can do this by providing a Configuration instance, that implements CachingConfigurer, and overrides the errorHandler() method.

For example:

@Configuration
@Ena1bleCaching
public class MyApp extends SpringBootServletInitializer  implements CachingConfigurer {

  @Override
  public CacheErrorHandler errorHandler() {

    return MyAppCacheErrorHandler();
  }

}

Exactly HOW you will then provide uninterrupted service is not clear to me - without duplicating the current sessions in your failover cache, it seems impossible.

If you are using ElasticCache, is it not possible to have AWS handle a replicated setup for you, so that if one node goes doen, the other can take over?

Layfield answered 2/9, 2017 at 21:21 Comment(1)
This does not work with [email protected]. Custom cache error handler is only called automatically when using @Cacheable annotation. spring-session-data-redis ignores it completelyTrisa

© 2022 - 2024 — McMap. All rights reserved.