Spring-session: SerializationFailedException after re-deploying with changed session object

Created on 26 Aug 2015  路  19Comments  路  Source: spring-projects/spring-session

After a re-deploy, because of spring-sessions, the sessions are alive. But if a class that has an object instance in the session changes, we get the following error.

LogLevel=ERROR; msg=Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception; category=o.a.c.c.C.[.[.[.[dispatcherServlet];
org.springframework.data.redis.serializer.SerializationException: Cannot deserialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is java.io.InvalidClassException: sb.se.domain.BillingPlanPromo; local class incompatible: stream classdesc serialVersionUID = 1760563701508754914, local class serialVersionUID = 140820402964695138

This error does make sense. However, the users ends up seeing a 500 page. Two solutions come to my mind here:

  1. Somehow ignore that particular session object class instance while keeping the session alive.
  2. Create a new session for the user

Is any of these solutions (or any other) possible?

Most helpful comment

I don't think that we can make ignoring any exception a default. However, I think that providing a class similar to InvalidClassExceptionSafeRepository is an option. A decorator class is appealing for a few reasons. Most notably that we do not need to duplicate the logic and ignoring the exceptions is totally decoupled from the implementation. I created #529 to track this so I'm closing this issue

All 19 comments

You have a number of options:

  • If you don't care about the objects between updates to the application, you can remove the sessions from Redis using:
$ redis-cli keys 'spring:session:*' | xargs redis-cli del
  • You can customize the Java serialization of your objects you place in session so that it is passive
  • You can override the RedisSerializer to use a custom strategy that reads and writes your objects in a passive manner
  • You could decorate the SessionRepository in an implementation that try catches retrieving the session and if it gets the InvalidClassException deletes the old session.
public class InvalidClassExceptionSafeRepository<S extends ExpiringSession> implements SessionRepository<S> {
    private final SessionRepository<S> repository;

    public InvalidClassExceptionSafeRepository(SessionRepository<S> repository) {
        this.repository = repository;
    }

    public S getSession(String id) {
        try {
            return repository.getSession(id);
        } catch(SerializationException e) {
            delete(id);
            return null;
        }
    }

    public S createSession() {
        return repository.createSession();
    }

    public void save(S session) {
        repository.save(session);
    }

    public void delete(String id) {
        repository.delete(id);
    }
}

@Primary
@Bean
public SessionRepository primarySessionRepository(RedisOperationsSessionRepository delegate) {
    return new InvalidClassExceptionSafeRepository(delegate);
}

Thanks for the input. I'm planning to go with Option 4, to decorate the SessionRepository. I added the code you have given. However, even though the bean is loaded, none of the methods are called during creating or getting the session. Is there any other bean or configuration that I might be missing?

I am instantiating the bean in HttpSessionConfig

@EnableRedisHttpSession // <1>
public class HttpSessionConfig {

    @Primary
    @Bean
    public SessionRepository primarySessionRepository(RedisOperationsSessionRepository delegate) {
        return new InvalidClassExceptionSafeRepository(delegate);
    }
}

Try adding @Configuration

Tried that as well. The bean does get instantiated. However it seems that internally Spring Session still uses the sessionRepository bean (instead of primarySessionRepository). That makes sense considering the fact that sessionRepository bean is of the type RedisOperationsSessionRepository, while we're creating a bean of the type SessionRepository.

public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate)

That shouldn't matter since the classes that consume it use the interface. I think the problem may be that you need to specify the generic types:

    @Primary
    @Bean
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public SessionRepository<? extends ExpiringSession> primarySessionRepository(RedisOperationsSessionRepository delegate) {
        return new InvalidClassExceptionSafeRepository(delegate);
    }

This is necessary because springSessionRepositoryFilter requests a SessionRepository<S extends ExpiringSession>

No luck :(.
I tested out the change with the samples/httpsession project, and it did not work even there. The changed project is given below:
https://app.box.com/s/g03o6cig4l1olwwhv387oi90do6a0bxc

Sorry...I missed your updated comment somehow. Did you end up getting this resolved?

Related #200

No, haven't looked into this in a while. I'll check again.

Ran into the same issue, opted for the solution with the Repository-wrapper, however I had to adapt it since the repository delete method would throw the same exception again, because it also tries to deserialize the session object first.

Here is my workaround-version:

public class InvalidClassExceptionSafeRepository<S extends ExpiringSession> implements SessionRepository<S> {
    private static final Logger LOG = getLogger(InvalidClassExceptionSafeRepository.class);
    private final SessionRepository<S> repository;
    private final RedisTemplate<String, ExpiringSession> sessionRedisTemplate;
    private final CounterService counterService;
    static final String BOUNDED_HASH_KEY_PREFIX = "spring:session:sessions:";

    public InvalidClassExceptionSafeRepository(SessionRepository<S> repository,
                                               RedisTemplate<String, ExpiringSession> sessionRedisTemplate,
                                               CounterService counterService) {
        this.repository = repository;
        this.sessionRedisTemplate = sessionRedisTemplate;
        this.counterService = counterService;
    }

    public S getSession(String id) {
        try {
            return repository.getSession(id);
        } catch(SerializationException e) {
            LOG.info("deleting non-deserializable session with key {}", id);
            // NOTE: deleting directly via redis instead of template since the repository.delete method would
            //  run into the same serializationissue again
            sessionRedisTemplate.delete(BOUNDED_HASH_KEY_PREFIX + id);
            counterService.increment("meter.sessions.deleteAfterDeserializationError");
            return null;
        }
    }

    public S createSession() {
        return repository.createSession();
    }

    public void save(S session) {
        repository.save(session);
    }

    public void delete(String id) {
        repository.delete(id);
    }
}

It's not as nice, since i have to duplicate the BOUNDED_HASH_KEY_PREFIX constant (package private, and I didn't want to use the same package name), but it gets the job done.

Thanks rwinch for the initial suggestion!

@NoUsername How do you instantiate the bean? I used

    @Primary
    @Bean
    public SessionRepository primarySessionRepository(RedisOperationsSessionRepository delegate, RedisTemplate<String, ExpiringSession> sessionRedisTemplate,
                                                      CounterService counterService) {
        return new InvalidClassExceptionSafeRepository(delegate, sessionRedisTemplate, counterService);
    }

However, when the session is created the bean is not used

Sorry for the long delay, this is how i instantiate it:

@Primary
    @Bean
    public SessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate, CounterService counterService) {
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
        sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
        // to prevent issues when upgrading spring-security versions (incompatible SecurityContext serialization)
        //  wrap the sessionRepository again
        return new InvalidClassExceptionSafeRepository(sessionRepository, sessionRedisTemplate, counterService);
    }

Clearing the session if an serialization exception occurs + warning log seems a reasonable default behavior. As it is now you have to clear the session manually or your application will break.

I think that a strategy would be ideal for serialization errors. Ideally default behaviour would be great with warn + log. Having a 500 is too harsh.

There is something very similar to this issue on org.springframework.context.event.SimpleApplicationEventMulticaster#errorHandler. There is multiple default implementions on org.springframework.scheduling.support.TaskUtils. What do you think @rwinch and @vpavic ?

I don't think that we can make ignoring any exception a default. However, I think that providing a class similar to InvalidClassExceptionSafeRepository is an option. A decorator class is appealing for a few reasons. Most notably that we do not need to duplicate the logic and ignoring the exceptions is totally decoupled from the implementation. I created #529 to track this so I'm closing this issue

While deleting session object I had to use prefix of spring session i.e "spring:sessions:" like below to make it work.

@Override
public S getSession(String id) {
    try {
        return repository.getSession(id);
    } catch(SerializationException e) {
        delete("spring:sessions:"+id);
        return null;
    }
}

Thanks

I tried all the suggestions mentioned above, nothing worked for me.

You have a number of options:

  • If you don't care about the objects between updates to the application, you can remove the sessions from Redis using:
$ redis-cli keys 'spring:session:*' | xargs redis-cli del
  • You can customize the Java serialization of your objects you place in session so that it is passive
  • You can override the RedisSerializer to use a custom strategy that reads and writes your objects in a passive manner
  • You could decorate the SessionRepository in an implementation that try catches retrieving the session and if it gets the InvalidClassException deletes the old session.
public class InvalidClassExceptionSafeRepository<S extends ExpiringSession> implements SessionRepository<S> {
    private final SessionRepository<S> repository;

    public InvalidClassExceptionSafeRepository(SessionRepository<S> repository) {
        this.repository = repository;
    }

    public S getSession(String id) {
        try {
            return repository.getSession(id);
        } catch(SerializationException e) {
            delete(id);
            return null;
        }
    }

    public S createSession() {
        return repository.createSession();
    }

    public void save(S session) {
        repository.save(session);
    }

    public void delete(String id) {
        repository.delete(id);
    }
}
@Primary
@Bean
public SessionRepository primarySessionRepository(RedisOperationsSessionRepository delegate) {
    return new InvalidClassExceptionSafeRepository(delegate);
}

When using this solution, I had huge performance issues when multiple users started using the app.
Hikari datasource's pool was getting locked until the 30000ms default timeout kicked in.
I was also using the default 10 active datasources pool.

Was this page helpful?
0 / 5 - 0 ratings