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 .Collections ;
26+ import  java .util .HashMap ;
27+ import  java .util .HashSet ;
2428import  java .util .Map ;
29+ import  java .util .Set ;
2530import  java .util .concurrent .ConcurrentHashMap ;
2631import  java .util .concurrent .ConcurrentMap ;
2732import  java .util .logging .Logger ;
28- import  java .util .stream .Collectors ;
2933import  org .openqa .selenium .NoSuchSessionException ;
34+ import  org .openqa .selenium .events .Event ;
3035import  org .openqa .selenium .events .EventBus ;
3136import  org .openqa .selenium .grid .config .Config ;
3237import  org .openqa .selenium .grid .data .NodeRemovedEvent ;
@@ -48,7 +53,7 @@ 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 ();
5257
5358  public  LocalSessionMap (Tracer  tracer , EventBus  bus ) {
5459    super (tracer );
@@ -59,23 +64,14 @@ public LocalSessionMap(Tracer tracer, EventBus bus) {
5964
6065    bus .addListener (
6166        NodeRemovedEvent .listener (
62-             nodeStatus  ->
63-                 nodeStatus .getSlots ().stream ()
64-                     .filter (slot  -> slot .getSession () != null )
65-                     .map (slot  -> slot .getSession ().getId ())
66-                     .forEach (this ::remove )));
67+             nodeStatus  -> {
68+               batchRemoveByUri (nodeStatus .getExternalUri (), NodeRemovedEvent .class );
69+             }));
6770
6871    bus .addListener (
6972        NodeRestartedEvent .listener (
7073            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 );
74+               batchRemoveByUri (previousNodeStatus .getExternalUri (), NodeRestartedEvent .class );
7975            }));
8076  }
8177
@@ -95,17 +91,23 @@ public boolean isReady() {
9591  public  boolean  add (Session  session ) {
9692    Require .nonNull ("Session" , session );
9793
94+     SessionId  id  = session .getId ();
95+     knownSessions .put (id , session );
96+ 
9897    try  (Span  span  = tracer .getCurrentContext ().createSpan ("local_sessionmap.add" )) {
9998      AttributeMap  attributeMap  = tracer .createAttributeMap ();
10099      attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
101-       SessionId  id  = session .getId ();
102100      SESSION_ID .accept (span , id );
103101      SESSION_ID_EVENT .accept (attributeMap , id );
104-       knownSessions .put (session .getId (), session );
105-       span .addEvent ("Added session into local session map" , attributeMap );
106102
107-       return  true ;
103+       String  sessionAddedMessage  =
104+           String .format (
105+               "Added session to local Session Map, Id: %s, Node: %s" , id , session .getUri ());
106+       span .addEvent (sessionAddedMessage , attributeMap );
107+       LOG .info (sessionAddedMessage );
108108    }
109+ 
110+     return  true ;
109111  }
110112
111113  @ Override 
@@ -116,23 +118,157 @@ public Session get(SessionId id) {
116118    if  (session  == null ) {
117119      throw  new  NoSuchSessionException ("Unable to find session with ID: "  + id );
118120    }
119- 
120121    return  session ;
121122  }
122123
123124  @ Override 
124125  public  void  remove (SessionId  id ) {
125126    Require .nonNull ("Session ID" , id );
126127
128+     Session  removedSession  = knownSessions .remove (id );
129+ 
127130    try  (Span  span  = tracer .getCurrentContext ().createSpan ("local_sessionmap.remove" )) {
128131      AttributeMap  attributeMap  = tracer .createAttributeMap ();
129132      attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
130133      SESSION_ID .accept (span , id );
131134      SESSION_ID_EVENT .accept (attributeMap , id );
132-       knownSessions .remove (id );
133-       String  sessionDeletedMessage  = "Deleted session from local Session Map" ;
135+ 
136+       String  sessionDeletedMessage  =
137+           String .format (
138+               "Deleted session from local Session Map, Id: %s, Node: %s" ,
139+               id ,
140+               removedSession  != null  ? String .valueOf (removedSession .getUri ()) : "unidentified" );
134141      span .addEvent (sessionDeletedMessage , attributeMap );
135-       LOG .info (String .format ("%s, Id: %s" , sessionDeletedMessage , id ));
142+       LOG .info (sessionDeletedMessage );
143+     }
144+   }
145+ 
146+   private  void  batchRemoveByUri (URI  externalUri , Class <? extends  Event > eventClass ) {
147+     Set <SessionId > sessionsToRemove  = knownSessions .getSessionsByUri (externalUri );
148+ 
149+     if  (sessionsToRemove .isEmpty ()) {
150+       return ; // Early return for empty operations - no tracing overhead 
151+     }
152+ 
153+     knownSessions .batchRemove (sessionsToRemove );
154+ 
155+     try  (Span  span  = tracer .getCurrentContext ().createSpan ("local_sessionmap.batch_remove" )) {
156+       AttributeMap  attributeMap  = tracer .createAttributeMap ();
157+       attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
158+       attributeMap .put ("event.class" , eventClass .getName ());
159+       attributeMap .put ("node.uri" , externalUri .toString ());
160+       attributeMap .put ("sessions.count" , sessionsToRemove .size ());
161+ 
162+       String  batchRemoveMessage  =
163+           String .format (
164+               "Batch removed %d sessions from local Session Map for Node %s (triggered by %s)" ,
165+               sessionsToRemove .size (), externalUri , eventClass .getSimpleName ());
166+       span .addEvent (batchRemoveMessage , attributeMap );
167+       LOG .info (batchRemoveMessage );
168+     }
169+   }
170+ 
171+   private  static  class  IndexedSessionMap  {
172+     private  final  ConcurrentMap <SessionId , Session > sessions  = new  ConcurrentHashMap <>();
173+     private  final  ConcurrentMap <URI , Set <SessionId >> sessionsByUri  = new  ConcurrentHashMap <>();
174+     private  final  Object  coordinationLock  = new  Object ();
175+ 
176+     public  Session  get (SessionId  id ) {
177+       return  sessions .get (id );
178+     }
179+ 
180+     public  Session  put (SessionId  id , Session  session ) {
181+       synchronized  (coordinationLock ) {
182+         Session  previous  = sessions .put (id , session );
183+ 
184+         if  (previous  != null  && previous .getUri () != null ) {
185+           cleanupUriIndex (previous .getUri (), id );
186+         }
187+ 
188+         URI  sessionUri  = session .getUri ();
189+         if  (sessionUri  != null ) {
190+           sessionsByUri .computeIfAbsent (sessionUri , k  -> ConcurrentHashMap .newKeySet ()).add (id );
191+         }
192+ 
193+         return  previous ;
194+       }
195+     }
196+ 
197+     public  Session  remove (SessionId  id ) {
198+       synchronized  (coordinationLock ) {
199+         Session  removed  = sessions .remove (id );
200+ 
201+         if  (removed  != null  && removed .getUri () != null ) {
202+           cleanupUriIndex (removed .getUri (), id );
203+         }
204+ 
205+         return  removed ;
206+       }
207+     }
208+ 
209+     public  void  batchRemove (Set <SessionId > sessionIds ) {
210+       synchronized  (coordinationLock ) {
211+         Map <URI , Set <SessionId >> uriToSessionIds  = new  HashMap <>();
212+ 
213+         // Single loop: remove sessions and collect URI mappings in one pass 
214+         for  (SessionId  id  : sessionIds ) {
215+           Session  session  = sessions .remove (id );
216+           if  (session  != null  && session .getUri () != null ) {
217+             uriToSessionIds .computeIfAbsent (session .getUri (), k  -> new  HashSet <>()).add (id );
218+           }
219+         }
220+ 
221+         // Clean up URI index for all affected URIs 
222+         for  (Map .Entry <URI , Set <SessionId >> entry  : uriToSessionIds .entrySet ()) {
223+           cleanupUriIndex (entry .getKey (), entry .getValue ());
224+         }
225+       }
226+     }
227+ 
228+     private  void  cleanupUriIndex (URI  uri , SessionId  sessionId ) {
229+       sessionsByUri .computeIfPresent (
230+           uri ,
231+           (key , sessionIds ) -> {
232+             sessionIds .remove (sessionId );
233+             return  sessionIds .isEmpty () ? null  : sessionIds ;
234+           });
235+     }
236+ 
237+     private  void  cleanupUriIndex (URI  uri , Set <SessionId > sessionIdsToRemove ) {
238+       sessionsByUri .computeIfPresent (
239+           uri ,
240+           (key , sessionIds ) -> {
241+             sessionIds .removeAll (sessionIdsToRemove );
242+             return  sessionIds .isEmpty () ? null  : sessionIds ;
243+           });
244+     }
245+ 
246+     public  Set <SessionId > getSessionsByUri (URI  uri ) {
247+       Set <SessionId > result  = sessionsByUri .get (uri );
248+       return  (result  != null  && !result .isEmpty ()) ? result  : Set .of ();
249+     }
250+ 
251+     public  Set <Map .Entry <SessionId , Session >> entrySet () {
252+       return  Collections .unmodifiableSet (sessions .entrySet ());
253+     }
254+ 
255+     public  Collection <Session > values () {
256+       return  Collections .unmodifiableCollection (sessions .values ());
257+     }
258+ 
259+     public  int  size () {
260+       return  sessions .size ();
261+     }
262+ 
263+     public  boolean  isEmpty () {
264+       return  sessions .isEmpty ();
265+     }
266+ 
267+     public  void  clear () {
268+       synchronized  (coordinationLock ) {
269+         sessions .clear ();
270+         sessionsByUri .clear ();
271+       }
136272    }
137273  }
138274}
0 commit comments