Describe the bug
InMemoryReactiveSessionRegistry can lose a session when saveSessionInformation and removeSessionInformation run concurrently for the same principal.
The per-principal session set is updated non-atomically:
saveSessionInformation calls .add(...) outside the computeIfAbsent mapping function.
removeSessionInformation prunes the principal key with a non-atomic get → isEmpty() → remove(principal).
To Reproduce
Principal P owns one session S1:
- Thread A removes
S1 — the set becomes empty, and A is about to remove key P.
- Thread B saves
S2 — computeIfAbsent(P) still sees key P, returns the same set, and adds S2.
- Thread A removes key
P — the set {S2} is orphaned; getAllSessions(P) returns empty and S2 is lost.
A latch-synchronized concurrent save/remove stress test reproduces this.
Expected behavior
A session added concurrently is not lost. The blocking SessionRegistryImpl already performs these updates atomically via compute/computeIfPresent; the reactive registry should do the same.
I will submit a PR with a regression test.
Describe the bug
InMemoryReactiveSessionRegistrycan lose a session whensaveSessionInformationandremoveSessionInformationrun concurrently for the same principal.The per-principal session set is updated non-atomically:
saveSessionInformationcalls.add(...)outside thecomputeIfAbsentmapping function.removeSessionInformationprunes the principal key with a non-atomicget→isEmpty()→remove(principal).To Reproduce
Principal
Powns one sessionS1:S1— the set becomes empty, and A is about to remove keyP.S2—computeIfAbsent(P)still sees keyP, returns the same set, and addsS2.P— the set{S2}is orphaned;getAllSessions(P)returns empty andS2is lost.A latch-synchronized concurrent save/remove stress test reproduces this.
Expected behavior
A session added concurrently is not lost. The blocking
SessionRegistryImplalready performs these updates atomically viacompute/computeIfPresent; the reactive registry should do the same.I will submit a PR with a regression test.