Skip to content

Commit 2b51760

Browse files
committed
[grid] Fix race condition in LocalSessionMap and improve logging
1 parent 8e46f06 commit 2b51760

File tree

1 file changed

+174
-25
lines changed

1 file changed

+174
-25
lines changed

java/src/org/openqa/selenium/grid/sessionmap/local/LocalSessionMap.java

Lines changed: 174 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
import static org.openqa.selenium.remote.RemoteTags.SESSION_ID;
2121
import static org.openqa.selenium.remote.RemoteTags.SESSION_ID_EVENT;
2222

23-
import java.util.List;
23+
import java.net.URI;
24+
import java.util.Collection;
25+
import java.util.HashMap;
26+
import java.util.HashSet;
2427
import java.util.Map;
28+
import java.util.Set;
2529
import java.util.concurrent.ConcurrentHashMap;
2630
import java.util.concurrent.ConcurrentMap;
31+
import java.util.concurrent.locks.ReadWriteLock;
32+
import java.util.concurrent.locks.ReentrantReadWriteLock;
2733
import java.util.logging.Logger;
28-
import java.util.stream.Collectors;
2934
import org.openqa.selenium.NoSuchSessionException;
3035
import org.openqa.selenium.events.EventBus;
3136
import org.openqa.selenium.grid.config.Config;
@@ -48,7 +53,8 @@ public class LocalSessionMap extends SessionMap {
4853
private static final Logger LOG = Logger.getLogger(LocalSessionMap.class.getName());
4954

5055
private final EventBus bus;
51-
private final ConcurrentMap<SessionId, Session> knownSessions = new ConcurrentHashMap<>();
56+
private final IndexedSessionMap knownSessions = new IndexedSessionMap();
57+
private final ReadWriteLock sessionMapLock = new ReentrantReadWriteLock();
5258

5359
public LocalSessionMap(Tracer tracer, EventBus bus) {
5460
super(tracer);
@@ -59,23 +65,14 @@ public LocalSessionMap(Tracer tracer, EventBus bus) {
5965

6066
bus.addListener(
6167
NodeRemovedEvent.listener(
62-
nodeStatus ->
63-
nodeStatus.getSlots().stream()
64-
.filter(slot -> slot.getSession() != null)
65-
.map(slot -> slot.getSession().getId())
66-
.forEach(this::remove)));
68+
nodeStatus -> {
69+
batchRemoveByUri(nodeStatus.getExternalUri());
70+
}));
6771

6872
bus.addListener(
6973
NodeRestartedEvent.listener(
7074
previousNodeStatus -> {
71-
List<SessionId> toRemove =
72-
knownSessions.entrySet().stream()
73-
.filter(
74-
(e) -> e.getValue().getUri().equals(previousNodeStatus.getExternalUri()))
75-
.map(Map.Entry::getKey)
76-
.collect(Collectors.toList());
77-
78-
toRemove.forEach(this::remove);
75+
batchRemoveByUri(previousNodeStatus.getExternalUri());
7976
}));
8077
}
8178

@@ -94,45 +91,197 @@ public boolean isReady() {
9491
@Override
9592
public boolean add(Session session) {
9693
Require.nonNull("Session", session);
94+
SessionId id = session.getId();
95+
sessionMapLock.writeLock().lock();
96+
try {
97+
knownSessions.put(id, session);
98+
} finally {
99+
sessionMapLock.writeLock().unlock();
100+
}
97101

98102
try (Span span = tracer.getCurrentContext().createSpan("local_sessionmap.add")) {
99103
AttributeMap attributeMap = tracer.createAttributeMap();
100104
attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(), getClass().getName());
101-
SessionId id = session.getId();
102105
SESSION_ID.accept(span, id);
103106
SESSION_ID_EVENT.accept(attributeMap, id);
104-
knownSessions.put(session.getId(), session);
105-
span.addEvent("Added session into local session map", attributeMap);
106-
107-
return true;
107+
span.addEvent("Added session into local Session Map", attributeMap);
108108
}
109+
110+
return true;
109111
}
110112

111113
@Override
112114
public Session get(SessionId id) {
113115
Require.nonNull("Session ID", id);
114116

115117
Session session = knownSessions.get(id);
116-
if (session == null) {
117-
throw new NoSuchSessionException("Unable to find session with ID: " + id);
118+
if (session != null) {
119+
return session;
118120
}
119121

120-
return session;
122+
sessionMapLock.readLock().lock();
123+
try {
124+
session = knownSessions.get(id);
125+
if (session == null) {
126+
throw new NoSuchSessionException("Unable to find session with ID: " + id);
127+
}
128+
return session;
129+
} finally {
130+
sessionMapLock.readLock().unlock();
131+
}
121132
}
122133

123134
@Override
124135
public void remove(SessionId id) {
125136
Require.nonNull("Session ID", id);
137+
sessionMapLock.writeLock().lock();
138+
try {
139+
knownSessions.remove(id);
140+
} finally {
141+
sessionMapLock.writeLock().unlock();
142+
}
126143

127144
try (Span span = tracer.getCurrentContext().createSpan("local_sessionmap.remove")) {
128145
AttributeMap attributeMap = tracer.createAttributeMap();
129146
attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(), getClass().getName());
130147
SESSION_ID.accept(span, id);
131148
SESSION_ID_EVENT.accept(attributeMap, id);
132-
knownSessions.remove(id);
133149
String sessionDeletedMessage = "Deleted session from local Session Map";
134150
span.addEvent(sessionDeletedMessage, attributeMap);
135151
LOG.info(String.format("%s, Id: %s", sessionDeletedMessage, id));
136152
}
137153
}
154+
155+
/** Batch remove sessions by URI with proper locking to prevent race conditions */
156+
private void batchRemoveByUri(URI externalUri) {
157+
sessionMapLock.writeLock().lock();
158+
Set<SessionId> sessionsToRemove;
159+
try {
160+
sessionsToRemove = knownSessions.getSessionsByUri(externalUri);
161+
if (!sessionsToRemove.isEmpty()) {
162+
knownSessions.batchRemove(sessionsToRemove);
163+
}
164+
} finally {
165+
sessionMapLock.writeLock().unlock();
166+
}
167+
168+
try (Span span = tracer.getCurrentContext().createSpan("local_sessionmap.batch_remove")) {
169+
AttributeMap attributeMap = tracer.createAttributeMap();
170+
attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(), getClass().getName());
171+
attributeMap.put("node.uri", externalUri.toString());
172+
attributeMap.put("sessions.count", sessionsToRemove.size());
173+
174+
if (!sessionsToRemove.isEmpty()) {
175+
String sessionDeletedMessage = "Batch removed sessions from local Session Map";
176+
span.addEvent(sessionDeletedMessage, attributeMap);
177+
LOG.info(
178+
String.format(
179+
"Batch removed %d sessions belonging to Node %s. List of sessions: %s",
180+
sessionsToRemove.size(), externalUri, sessionsToRemove));
181+
} else {
182+
String noSessionsMessage =
183+
String.format(
184+
"No sessions found to remove from local Session Map for Node %s", externalUri);
185+
span.addEvent(noSessionsMessage, attributeMap);
186+
LOG.info(noSessionsMessage);
187+
}
188+
}
189+
}
190+
191+
/** Custom ConcurrentMap implementation that automatically maintains a URI-to-SessionId index */
192+
private static class IndexedSessionMap {
193+
private final ConcurrentMap<SessionId, Session> sessions = new ConcurrentHashMap<>();
194+
private final ConcurrentMap<URI, Set<SessionId>> sessionsByUri = new ConcurrentHashMap<>();
195+
196+
public Session get(SessionId id) {
197+
return sessions.get(id);
198+
}
199+
200+
public Session put(SessionId id, Session session) {
201+
Session previous = sessions.put(id, session);
202+
203+
if (previous != null && previous.getUri() != null) {
204+
sessionsByUri.computeIfPresent(
205+
previous.getUri(),
206+
(k, sessionIds) -> {
207+
sessionIds.remove(id);
208+
return sessionIds.isEmpty() ? null : sessionIds;
209+
});
210+
}
211+
212+
URI sessionUri = session.getUri();
213+
if (sessionUri != null) {
214+
sessionsByUri.computeIfAbsent(sessionUri, k -> ConcurrentHashMap.newKeySet()).add(id);
215+
}
216+
217+
return previous;
218+
}
219+
220+
public Session remove(SessionId id) {
221+
Session removed = sessions.remove(id);
222+
223+
if (removed != null && removed.getUri() != null) {
224+
sessionsByUri.computeIfPresent(
225+
removed.getUri(),
226+
(k, sessionIds) -> {
227+
sessionIds.remove(id);
228+
return sessionIds.isEmpty() ? null : sessionIds;
229+
});
230+
}
231+
232+
return removed;
233+
}
234+
235+
public void batchRemove(Set<SessionId> sessionIds) {
236+
Map<URI, Set<SessionId>> uriToSessionIds = new HashMap<>();
237+
238+
for (SessionId id : sessionIds) {
239+
Session session = sessions.get(id);
240+
if (session != null && session.getUri() != null) {
241+
uriToSessionIds.computeIfAbsent(session.getUri(), k -> new HashSet<>()).add(id);
242+
}
243+
}
244+
245+
for (SessionId id : sessionIds) {
246+
sessions.remove(id);
247+
}
248+
249+
for (Map.Entry<URI, Set<SessionId>> entry : uriToSessionIds.entrySet()) {
250+
URI uri = entry.getKey();
251+
Set<SessionId> idsToRemove = entry.getValue();
252+
253+
sessionsByUri.computeIfPresent(
254+
uri,
255+
(k, existingIds) -> {
256+
existingIds.removeAll(idsToRemove);
257+
return existingIds.isEmpty() ? null : existingIds;
258+
});
259+
}
260+
}
261+
262+
public Set<Map.Entry<SessionId, Session>> entrySet() {
263+
return sessions.entrySet();
264+
}
265+
266+
public Collection<Session> values() {
267+
return sessions.values();
268+
}
269+
270+
public Set<SessionId> getSessionsByUri(URI uri) {
271+
return sessionsByUri.getOrDefault(uri, Set.of());
272+
}
273+
274+
public int size() {
275+
return sessions.size();
276+
}
277+
278+
public boolean isEmpty() {
279+
return sessions.isEmpty();
280+
}
281+
282+
public void clear() {
283+
sessions.clear();
284+
sessionsByUri.clear();
285+
}
286+
}
138287
}

0 commit comments

Comments
 (0)