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