2020import static org .openqa .selenium .remote .RemoteTags .SESSION_ID ;
2121import 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 ;
2427import java .util .Map ;
28+ import java .util .Set ;
2529import java .util .concurrent .ConcurrentHashMap ;
2630import java .util .concurrent .ConcurrentMap ;
31+ import java .util .concurrent .locks .ReadWriteLock ;
32+ import java .util .concurrent .locks .ReentrantReadWriteLock ;
2733import java .util .logging .Logger ;
28- import java .util .stream .Collectors ;
2934import org .openqa .selenium .NoSuchSessionException ;
35+ import org .openqa .selenium .events .Event ;
3036import org .openqa .selenium .events .EventBus ;
3137import org .openqa .selenium .grid .config .Config ;
3238import 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