Skip to content

Extremely rare error due to session expiration date and session storage expiration date being the same. #2786

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
yu-shiba opened this issue Feb 8, 2024 · 2 comments
Assignees
Labels
status: declined A suggestion or change that we don't feel we should currently apply type: bug A general bug

Comments

@yu-shiba
Copy link

yu-shiba commented Feb 8, 2024

Describe the bug
When running with Redis as session storage for Spring Session, the following exception may occur in rare cases.

IllegalStateException("Session was invalidated")

To Reproduce
To reproduce the problem, access the session before the session timeout, stop the session with debugger until the session storage expires, and then restart the session.

Expected behavior
After investigating this problem, we found that it is caused by session storage expiring between retrieving a session just before the session expires and saving the session.

The current session and session storage expiration are

Session storage expiration == Session expiration

but given the above lag between when sessions are retrieved and when session storage expiration is updated, we think the session storage expiration should be longer than the session expiration.

Ideally, it should be the session expiration date plus at least the maximum execution time, but since the maximum execution time depends on system requirements, we think it should be configurable in the properties.

Sample

The above reproduction procedure will confirm this, but we will create it if necessary.

@yu-shiba yu-shiba added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Feb 8, 2024
@marcusdacoregio
Copy link
Contributor

Hi, @yu-shiba. This behavior is expected, see #1277.
As of now, we cannot try to save a session that is not new and does not exists on the session storage anymore. I don't think a fixed configurable delay would also solve the problem as the time between the retrieval and the save may vary a lot depending on the request.

A workaround if you do not want the IllegalStateException behavior is to implement your own repository that swallows the exception. To do that you can use the @EnableSpringHttpSession annotation that will configure Spring Session filters for you and expects a SessionRepository bean.

@Configuration
@EnableSpringHttpSession
public class SessionConfig {

        @Bean
	SafeSessionRepository<?> repository(RedisConnectionFactory redisConnectionFactory) {
		RedisSessionRepository delegate = new RedisSessionRepository(createRedisTemplate(redisConnectionFactory));
		return new SafeSessionRepository<>(delegate);
	}

	private RedisTemplate<String, Object> createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
                // set the properties accordingly to your needs
		redisTemplate.setKeySerializer(RedisSerializer.string());
		redisTemplate.setHashKeySerializer(RedisSerializer.string());
		redisTemplate.setConnectionFactory(redisConnectionFactory);
		redisTemplate.afterPropertiesSet();
		return redisTemplate;
	}

	static class SafeSessionRepository<T extends Session> implements SessionRepository<T> {

		private final SessionRepository<T> delegate;

		SafeSessionRepository(SessionRepository<T> delegate) {
			this.delegate = delegate;
		}

		@Override
		public T createSession() {
			return this.delegate.createSession();
		}

		@Override
		public void save(T session) {
			try {
				this.delegate.save(session);
			} catch (IllegalStateException ex) {
				// your behavior here
			}
		}

		@Override
		public T findById(String id) {
			return this.delegate.findById(id);
		}

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

	}


}

I'll close this because, for now, we won't add any configurable delay as a solution for this problem.

@marcusdacoregio marcusdacoregio self-assigned this Feb 8, 2024
@marcusdacoregio marcusdacoregio added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged labels Feb 8, 2024
@yu-shiba
Copy link
Author

yu-shiba commented Feb 9, 2024

Hi, @marcusdacoregio , #1277 read. Perhaps I should have added the contents of this ticket to the ticket over there.

The issue I would like to raise in this ticket is whether it is possible to improve upon the problem caused by the session and session storage expiration date being identical, which is one of the reasons why a session is determined to be invalid.
And this essentially leads to the question of what the session storage expiration date is for.

In a case like this, the session expiration date and the session storage expiration date are considered the same, and the session is determined to be an invalidated session.
This means that the session expiration date is also managed by the session information (lastAccessedTime and maxInactiveInterval) and the session storage expiration date.
This dual management may be undesirable.

I think the session storage expiration date should be set for session trash cleanup and should have nothing to do with the session expiration date.

In my project I try to avoid this problem by delegating RedisTemplate to adjust the expireAt method.
The maximum execution time is difficult to predict, but I try to add a session timeout (spring.sesstion.timeout) since it can never exceed the session lifetime.

@Configuration
public class RedisSessionConfig extends RedisHttpSessionConfiguration {
 
    @Autowired
    private SessionProperties sessionProperties;

    @Override
    protected Duration getMaxInactiveInterval() {
        return sessionProperties.getTimeout();
    }

    @Override
    protected RedisTemplate<String, Object> createRedisTemplate() {
        return new SkewedRedisTemplate<String, Object>(super.createRedisTemplate(), getMaxInactiveInterval());
    }

    @RequiredArgsConstructor
    public static class SkewedRedisTemplate<K, V> extends RedisTemplate<K, V> {
        @Delegate
        private final RedisOperations<K, V> delegate;

        private final Duration skew;

        @Override
        public Boolean expire(K key, long timeout, TimeUnit unit) {
            var skewedTimeout = skew.plus(timeout, unit.toChronoUnit()).get(unit.toChronoUnit());
            return delegate.expire(key, skewedTimeout, unit);
        }

        @Override
        public Boolean expire(K key, Duration timeout) {
            return super.expire(key, timeout);
        }

        @Override
        public Boolean expireAt(K key, Date date) {
            var skewedDate = DateUtils.addSeconds(date, (int) skew.getSeconds());
            return delegate.expireAt(key, skewedDate);
        }

        @Override
        public Boolean expireAt(K key, Instant expireAt) {
            return super.expireAt(key, expireAt);
        }

        @Override
        public <T extends Closeable> T executeWithStickyConnection(RedisCallback<T> callback) {
            return delegate.executeWithStickyConnection(callback);
        }
    }
}

With implementation of this measure, the possibility of an IllegalStateException("Session was invalidated") being thrown has been reduced to a minimum.
The only time this exception is thrown is when Redis reaches its memory limit and is evicted, which is easy to monitor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply type: bug A general bug
Projects
None yet
Development

No branches or pull requests

2 participants