Skip to content

Commit 2f0b580

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

File tree

1 file changed

+185
-27
lines changed

1 file changed

+185
-27
lines changed

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

Lines changed: 185 additions & 27 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,205 @@ 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

115118
Session session = knownSessions.get(id);
116-
if (session == null) {
117-
throw new NoSuchSessionException("Unable to find session with ID: " + id);
119+
if (session != null) {
120+
return session;
118121
}
119122

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

123135
@Override
124136
public void remove(SessionId id) {
125137
Require.nonNull("Session ID", id);
138+
Session removedSession;
139+
sessionMapLock.writeLock().lock();
140+
try {
141+
removedSession = knownSessions.remove(id);
142+
} finally {
143+
sessionMapLock.writeLock().unlock();
144+
}
126145

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

0 commit comments

Comments
 (0)