Skip to content

Commit 4c8bca8

Browse files
committed
[grid] Fix race condition in LocalSessionMap and improve logging
Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent 8e46f06 commit 4c8bca8

File tree

1 file changed

+183
-28
lines changed

1 file changed

+183
-28
lines changed

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

Lines changed: 183 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@
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;
35+
import org.openqa.selenium.events.Event;
3036
import org.openqa.selenium.events.EventBus;
3137
import org.openqa.selenium.grid.config.Config;
3238
import org.openqa.selenium.grid.data.NodeRemovedEvent;
@@ -48,7 +54,8 @@ public class LocalSessionMap extends SessionMap {
4854
private static final Logger LOG = Logger.getLogger(LocalSessionMap.class.getName());
4955

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

5360
public LocalSessionMap(Tracer tracer, EventBus bus) {
5461
super(tracer);
@@ -59,23 +66,14 @@ public LocalSessionMap(Tracer tracer, EventBus bus) {
5966

6067
bus.addListener(
6168
NodeRemovedEvent.listener(
62-
nodeStatus ->
63-
nodeStatus.getSlots().stream()
64-
.filter(slot -> slot.getSession() != null)
65-
.map(slot -> slot.getSession().getId())
66-
.forEach(this::remove)));
69+
nodeStatus -> {
70+
batchRemoveByUri(nodeStatus.getExternalUri(), NodeRemovedEvent.class);
71+
}));
6772

6873
bus.addListener(
6974
NodeRestartedEvent.listener(
7075
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);
76+
batchRemoveByUri(previousNodeStatus.getExternalUri(), NodeRestartedEvent.class);
7977
}));
8078
}
8179

@@ -94,45 +92,202 @@ public boolean isReady() {
9492
@Override
9593
public boolean add(Session session) {
9694
Require.nonNull("Session", session);
95+
SessionId id = session.getId();
96+
sessionMapLock.writeLock().lock();
97+
try {
98+
knownSessions.put(id, session);
99+
} finally {
100+
sessionMapLock.writeLock().unlock();
101+
}
97102

98103
try (Span span = tracer.getCurrentContext().createSpan("local_sessionmap.add")) {
99104
AttributeMap attributeMap = tracer.createAttributeMap();
100105
attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(), getClass().getName());
101-
SessionId id = session.getId();
102106
SESSION_ID.accept(span, id);
103107
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;
108+
span.addEvent("Added session into local Session Map", attributeMap);
108109
}
110+
111+
return true;
109112
}
110113

111114
@Override
112115
public Session get(SessionId id) {
113116
Require.nonNull("Session ID", id);
114117

115-
Session session = knownSessions.get(id);
116-
if (session == null) {
117-
throw new NoSuchSessionException("Unable to find session with ID: " + id);
118+
Session session;
119+
sessionMapLock.readLock().lock();
120+
try {
121+
session = knownSessions.get(id);
122+
if (session == null) {
123+
throw new NoSuchSessionException("Unable to find session with ID: " + id);
124+
}
125+
} finally {
126+
sessionMapLock.readLock().unlock();
118127
}
119-
120128
return session;
121129
}
122130

123131
@Override
124132
public void remove(SessionId id) {
125133
Require.nonNull("Session ID", id);
134+
Session removedSession;
135+
sessionMapLock.writeLock().lock();
136+
try {
137+
removedSession = knownSessions.remove(id);
138+
} finally {
139+
sessionMapLock.writeLock().unlock();
140+
}
126141

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

0 commit comments

Comments
 (0)