Skip to content

SerializationFailedException after re-deploying with changed session object #280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
nsdiv opened this issue Aug 25, 2015 · 19 comments
Closed

Comments

@nsdiv
Copy link

nsdiv commented Aug 25, 2015

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?

@rwinch
Copy link
Member

rwinch commented Aug 26, 2015

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);
}

@nsdiv
Copy link
Author

nsdiv commented Aug 26, 2015

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);
    }
}

@rwinch
Copy link
Member

rwinch commented Aug 26, 2015

Try adding @Configuration

@nsdiv
Copy link
Author

nsdiv commented Aug 26, 2015

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)

@rwinch
Copy link
Member

rwinch commented Aug 26, 2015

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>

@nsdiv
Copy link
Author

nsdiv commented Aug 26, 2015

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

@rwinch
Copy link
Member

rwinch commented Nov 5, 2015

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

@rwinch
Copy link
Member

rwinch commented Nov 5, 2015

Related #200

@nsdiv
Copy link
Author

nsdiv commented Nov 6, 2015

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

@NoUsername
Copy link

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!

@nsdiv
Copy link
Author

nsdiv commented Feb 19, 2016

@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

@NoUsername
Copy link

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);
    }

@cyril265
Copy link

cyril265 commented May 23, 2016

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.

@cemo
Copy link

cemo commented May 23, 2016

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.

@cemo
Copy link

cemo commented May 23, 2016

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 ?

@rwinch
Copy link
Member

rwinch commented May 23, 2016

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

@rwinch rwinch closed this as completed May 23, 2016
@privatejava
Copy link

privatejava commented Mar 30, 2017

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

@vishnusk1995
Copy link

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

@baniwel
Copy link

baniwel commented Jan 28, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants