Skip to content

Commit 19d7c03

Browse files
committed
Fix Race Condition in Integration Tests Using Redis SessionEventRegistry
Closes gh-3398
1 parent 755119e commit 19d7c03

File tree

4 files changed

+47
-24
lines changed

4 files changed

+47
-24
lines changed

spring-session-data-redis/src/integration-test/java/org/springframework/session/data/SessionEventRegistry.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,29 @@
1616

1717
package org.springframework.session.data;
1818

19+
import java.util.ArrayList;
1920
import java.util.HashMap;
21+
import java.util.List;
2022
import java.util.Map;
2123
import java.util.concurrent.ConcurrentHashMap;
2224
import java.util.concurrent.ConcurrentMap;
25+
import java.util.concurrent.TimeUnit;
26+
27+
import org.jetbrains.annotations.Nullable;
2328

2429
import org.springframework.context.ApplicationListener;
2530
import org.springframework.session.events.AbstractSessionEvent;
2631

2732
public class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
2833

29-
private Map<String, AbstractSessionEvent> events = new HashMap<>();
34+
private Map<String, List<AbstractSessionEvent>> events = new HashMap<>();
3035

3136
private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();
3237

3338
@Override
3439
public void onApplicationEvent(AbstractSessionEvent event) {
3540
String sessionId = event.getSessionId();
36-
this.events.put(sessionId, event);
41+
this.events.computeIfAbsent(sessionId, (key) -> new ArrayList<>()).add(event);
3742
Object lock = getLock(sessionId);
3843
synchronized (lock) {
3944
lock.notifyAll();
@@ -45,24 +50,41 @@ public void clear() {
4550
this.locks.clear();
4651
}
4752

48-
public boolean receivedEvent(String sessionId) throws InterruptedException {
49-
return waitForEvent(sessionId) != null;
50-
}
51-
52-
@SuppressWarnings("unchecked")
53-
public <E extends AbstractSessionEvent> E getEvent(String sessionId) throws InterruptedException {
54-
return (E) waitForEvent(sessionId);
53+
public <E extends AbstractSessionEvent> boolean receivedEvent(String sessionId, Class<E> type)
54+
throws InterruptedException {
55+
return waitForEvent(sessionId, type) != null;
5556
}
5657

5758
@SuppressWarnings("unchecked")
58-
private <E extends AbstractSessionEvent> E waitForEvent(String sessionId) throws InterruptedException {
59+
public <E extends AbstractSessionEvent> E waitForEvent(String sessionId, Class<E> type)
60+
throws InterruptedException {
5961
Object lock = getLock(sessionId);
62+
long waitInMs = TimeUnit.SECONDS.toMillis(10);
63+
long start = System.currentTimeMillis();
64+
boolean doneWaiting = false;
6065
synchronized (lock) {
61-
if (!this.events.containsKey(sessionId)) {
62-
lock.wait(10000);
66+
while (!doneWaiting) {
67+
E result = getEvent(sessionId, type);
68+
if (result == null) {
69+
// wait until timeout or notified
70+
// might need to continue trying if the notification
71+
// was for a different event
72+
lock.wait(waitInMs);
73+
}
74+
long now = System.currentTimeMillis();
75+
doneWaiting = (now - start) >= waitInMs;
6376
}
77+
return getEvent(sessionId, type);
6478
}
65-
return (E) this.events.get(sessionId);
79+
}
80+
81+
private <E extends AbstractSessionEvent> @Nullable E getEvent(String sessionId, Class<E> type) {
82+
List<AbstractSessionEvent> events = this.events.get(sessionId);
83+
E result = (events != null) ? (E) events.stream()
84+
.filter((event) -> type.isAssignableFrom(event.getClass()))
85+
.findFirst()
86+
.orElse(null) : null;
87+
return result;
6688
}
6789

6890
private Object getLock(String sessionId) {

spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/ReactiveRedisIndexedSessionRepositoryConfigurationITests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ void onSessionCreatedWhenUsingJsonSerializerThenEventDeserializedCorrectly() thr
101101
RedisSession session = this.repository.createSession().block();
102102
this.repository.save(session).block();
103103
SessionEventRegistry registry = this.context.getBean(SessionEventRegistry.class);
104-
SessionCreatedEvent event = registry.getEvent(session.getId());
104+
SessionCreatedEvent event = registry.waitForEvent(session.getId(), SessionCreatedEvent.class);
105105
Session eventSession = event.getSession();
106106
assertThat(eventSession).usingRecursiveComparison()
107107
.withComparatorForFields(new InstantComparator(), "cached.creationTime", "cached.lastAccessedTime")

spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/ReactiveRedisIndexedSessionRepositoryITests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ void saveWhenSuccessThenSessionCreatedEvent() throws InterruptedException {
124124

125125
this.repository.save(session).block();
126126

127-
SessionCreatedEvent event = this.eventRegistry.getEvent(session.getId());
127+
SessionCreatedEvent event = this.eventRegistry.waitForEvent(session.getId(), SessionCreatedEvent.class);
128128
assertThat(event).isNotNull();
129129
RedisSession eventSession = event.getSession();
130130
compareSessions(session, eventSession);
@@ -168,7 +168,7 @@ void findByPrincipalNameWhenExpireKeyEventThenRemovesIndexAndSessionExpiredEvent
168168
assertThat(this.redis.expire(key, Duration.ofSeconds(1)).block()).isTrue();
169169

170170
await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> {
171-
SessionExpiredEvent event = this.eventRegistry.getEvent(toSave.getId());
171+
SessionExpiredEvent event = this.eventRegistry.waitForEvent(toSave.getId(), SessionExpiredEvent.class);
172172
RedisSession eventSession = event.getSession();
173173
Map<String, RedisSession> findByPrincipalName = this.repository
174174
.findByIndexNameAndIndexValue(INDEX_NAME, principalName)
@@ -206,7 +206,7 @@ void findByPrincipalNameWhenDeletedKeyEventThenRemovesIndex() {
206206
.block();
207207
assertThat(findByPrincipalName).hasSize(0);
208208
assertThat(findByPrincipalName.keySet()).doesNotContain(toSave.getId());
209-
SessionDeletedEvent event = this.eventRegistry.getEvent(toSave.getId());
209+
SessionDeletedEvent event = this.eventRegistry.waitForEvent(toSave.getId(), SessionDeletedEvent.class);
210210
assertThat(event).isNotNull();
211211
RedisSession eventSession = event.getSession();
212212
compareSessions(toSave, eventSession);

spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisIndexedSessionRepositoryITests.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ void saves() throws InterruptedException {
111111

112112
this.repository.save(toSave);
113113

114-
assertThat(this.registry.receivedEvent(toSave.getId())).isTrue();
114+
assertThat(this.registry.receivedEvent(toSave.getId(), SessionCreatedEvent.class)).isTrue();
115115
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).contains(toSave.getId());
116116

117-
SessionCreatedEvent createdEvent = this.registry.getEvent(toSave.getId());
117+
SessionCreatedEvent createdEvent = this.registry.waitForEvent(toSave.getId(), SessionCreatedEvent.class);
118118
Session session = createdEvent.getSession();
119119

120120
assertThat(session.getId()).isEqualTo(toSave.getId());
@@ -127,11 +127,10 @@ void saves() throws InterruptedException {
127127
this.repository.deleteById(toSave.getId());
128128

129129
assertThat(this.repository.findById(toSave.getId())).isNull();
130-
assertThat(this.registry.<SessionDestroyedEvent>getEvent(toSave.getId()))
131-
.isInstanceOf(SessionDestroyedEvent.class);
132130
assertThat(this.redis.boundSetOps(usernameSessionKey).members()).doesNotContain(toSave.getId());
133131

134-
assertThat(this.registry.getEvent(toSave.getId()).getSession().<String>getAttribute(expectedAttributeName))
132+
SessionDestroyedEvent destroyedEvent = this.registry.waitForEvent(toSave.getId(), SessionDestroyedEvent.class);
133+
assertThat(destroyedEvent.getSession().<String>getAttribute(expectedAttributeName))
135134
.isEqualTo(expectedAttributeValue);
136135
}
137136

@@ -171,7 +170,8 @@ void findByPrincipalName() throws Exception {
171170
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
172171

173172
this.repository.deleteById(toSave.getId());
174-
assertThat(this.registry.receivedEvent(toSave.getId())).isTrue();
173+
boolean sessionDestroyed = this.registry.receivedEvent(toSave.getId(), SessionDestroyedEvent.class);
174+
assertThat(sessionDestroyed).isTrue();
175175

176176
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, principalName);
177177

@@ -334,7 +334,8 @@ void findBySecurityPrincipalName() throws Exception {
334334
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
335335

336336
this.repository.deleteById(toSave.getId());
337-
assertThat(this.registry.receivedEvent(toSave.getId())).isTrue();
337+
boolean sessionDestroyed = this.registry.receivedEvent(toSave.getId(), SessionDestroyedEvent.class);
338+
assertThat(sessionDestroyed).isTrue();
338339

339340
findByPrincipalName = this.repository.findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName());
340341

0 commit comments

Comments
 (0)