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,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