Skip to content

Make InMemoryReactiveSessionRegistry updates atomic#19339

Open
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/reactivesessionregistry-atomic-save-remove
Open

Make InMemoryReactiveSessionRegistry updates atomic#19339
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/reactivesessionregistry-atomic-save-remove

Conversation

@junhyeong9812

Copy link
Copy Markdown

Overview

InMemoryReactiveSessionRegistry can lose a session when saveSessionInformation and removeSessionInformation run concurrently for the same principal.

Problem

The per-principal session set was updated non-atomically:

  • saveSessionInformation added the session id outside the computeIfAbsent mapping function.
  • removeSessionInformation pruned the principal key with a non-atomic getisEmpty()remove(principal).

Interleaving (principal P owns one session S1):

  1. Thread A removes S1; the set becomes empty and A is about to remove key P.
  2. Thread B saves S2: computeIfAbsent(P) still sees key P, returns the same set, and adds S2.
  3. Thread A removes key P → the set {S2} is orphaned; getAllSessions(P) is empty and S2 is lost.

The blocking SessionRegistryImpl already avoids this by performing both updates atomically with compute/computeIfPresent (on the same ConcurrentMap field shape and constructor); the reactive registry did not.

Fix

Mirror the blocking SessionRegistryImpl: perform the add inside compute, and the remove-and-prune inside computeIfPresent (returning null to drop the key only while the set is empty). This makes the per-principal updates atomic.

A 1000-iteration, latch-synchronized concurrent save/remove stress test is added; it fails on the previous code and passes with the fix.

Closes gh-19338

saveSessionInformation added the session id outside the computeIfAbsent
mapping function, and removeSessionInformation pruned the principal key
with a non-atomic isEmpty()-then-remove. A concurrent save and remove
for the same principal could therefore drop a newly added session. Use
compute and computeIfPresent so the per-principal updates are atomic,
matching the blocking SessionRegistryImpl.

Closes spring-projectsgh-19338

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 14, 2026
(key, sessionsUsedByPrincipal) -> {
sessionsUsedByPrincipal.remove(sessionId);
return sessionsUsedByPrincipal.isEmpty() ? null : sessionsUsedByPrincipal;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private Set<String> addSessionToSet(String principal, String sessionId, Set<String> existing) {
    if (existing == null) {
        existing = new CopyOnWriteArraySet<>();
    }
    existing.add(sessionId);
    return existing;
}

private Set<String> removeSessionFromSet(Set<String> sessions, String sessionId) {
    sessions.remove(sessionId);
    return sessions.isEmpty() ? null : sessions;
}

public Mono<ReactiveSessionInformation> removeSessionInformation(String sessionId) {
return getSessionInformation(sessionId).doOnNext((sessionInformation) -> {
this.sessionById.remove(sessionId);
Set<String> sessionsUsedByPrincipal = this.sessionIdsByPrincipal.get(sessionInformation.getPrincipal());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sessionsUsedByPrincipal-> sessions

.collectList()
.block();
assertThat(sessions).extracting(ReactiveSessionInformation::getSessionId).contains(added);
this.sessionRegistry.removeSessionInformation(added).block();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this iterative block can be declared as a function - would be modular

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InMemoryReactiveSessionRegistry can lose a session under concurrent save/remove

3 participants