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 ;
3035import org .openqa .selenium .events .EventBus ;
3136import org .openqa .selenium .grid .config .Config ;
@@ -48,7 +53,8 @@ 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 ();
57+ private final ReadWriteLock sessionMapLock = new ReentrantReadWriteLock ();
5258
5359 public LocalSessionMap (Tracer tracer , EventBus bus ) {
5460 super (tracer );
@@ -59,23 +65,14 @@ public LocalSessionMap(Tracer tracer, EventBus bus) {
5965
6066 bus .addListener (
6167 NodeRemovedEvent .listener (
62- nodeStatus ->
63- nodeStatus .getSlots ().stream ()
64- .filter (slot -> slot .getSession () != null )
65- .map (slot -> slot .getSession ().getId ())
66- .forEach (this ::remove )));
68+ nodeStatus -> {
69+ batchRemoveByUri (nodeStatus .getExternalUri ());
70+ }));
6771
6872 bus .addListener (
6973 NodeRestartedEvent .listener (
7074 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 );
75+ batchRemoveByUri (previousNodeStatus .getExternalUri ());
7976 }));
8077 }
8178
@@ -94,45 +91,197 @@ public boolean isReady() {
9491 @ Override
9592 public boolean add (Session session ) {
9693 Require .nonNull ("Session" , session );
94+ SessionId id = session .getId ();
95+ sessionMapLock .writeLock ().lock ();
96+ try {
97+ knownSessions .put (id , session );
98+ } finally {
99+ sessionMapLock .writeLock ().unlock ();
100+ }
97101
98102 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.add" )) {
99103 AttributeMap attributeMap = tracer .createAttributeMap ();
100104 attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
101- SessionId id = session .getId ();
102105 SESSION_ID .accept (span , id );
103106 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 ;
107+ span .addEvent ("Added session into local Session Map" , attributeMap );
108108 }
109+
110+ return true ;
109111 }
110112
111113 @ Override
112114 public Session get (SessionId id ) {
113115 Require .nonNull ("Session ID" , id );
114116
115117 Session session = knownSessions .get (id );
116- if (session = = null ) {
117- throw new NoSuchSessionException ( "Unable to find session with ID: " + id ) ;
118+ if (session ! = null ) {
119+ return session ;
118120 }
119121
120- return session ;
122+ sessionMapLock .readLock ().lock ();
123+ try {
124+ session = knownSessions .get (id );
125+ if (session == null ) {
126+ throw new NoSuchSessionException ("Unable to find session with ID: " + id );
127+ }
128+ return session ;
129+ } finally {
130+ sessionMapLock .readLock ().unlock ();
131+ }
121132 }
122133
123134 @ Override
124135 public void remove (SessionId id ) {
125136 Require .nonNull ("Session ID" , id );
137+ sessionMapLock .writeLock ().lock ();
138+ try {
139+ knownSessions .remove (id );
140+ } finally {
141+ sessionMapLock .writeLock ().unlock ();
142+ }
126143
127144 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.remove" )) {
128145 AttributeMap attributeMap = tracer .createAttributeMap ();
129146 attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
130147 SESSION_ID .accept (span , id );
131148 SESSION_ID_EVENT .accept (attributeMap , id );
132- knownSessions .remove (id );
133149 String sessionDeletedMessage = "Deleted session from local Session Map" ;
134150 span .addEvent (sessionDeletedMessage , attributeMap );
135151 LOG .info (String .format ("%s, Id: %s" , sessionDeletedMessage , id ));
136152 }
137153 }
154+
155+ /** Batch remove sessions by URI with proper locking to prevent race conditions */
156+ private void batchRemoveByUri (URI externalUri ) {
157+ sessionMapLock .writeLock ().lock ();
158+ Set <SessionId > sessionsToRemove ;
159+ try {
160+ sessionsToRemove = knownSessions .getSessionsByUri (externalUri );
161+ if (!sessionsToRemove .isEmpty ()) {
162+ knownSessions .batchRemove (sessionsToRemove );
163+ }
164+ } finally {
165+ sessionMapLock .writeLock ().unlock ();
166+ }
167+
168+ try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.batch_remove" )) {
169+ AttributeMap attributeMap = tracer .createAttributeMap ();
170+ attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
171+ attributeMap .put ("node.uri" , externalUri .toString ());
172+ attributeMap .put ("sessions.count" , sessionsToRemove .size ());
173+
174+ if (!sessionsToRemove .isEmpty ()) {
175+ String sessionDeletedMessage = "Batch removed sessions from local Session Map" ;
176+ span .addEvent (sessionDeletedMessage , attributeMap );
177+ LOG .info (
178+ String .format (
179+ "Batch removed %d sessions belonging to Node %s. List of sessions: %s" ,
180+ sessionsToRemove .size (), externalUri , sessionsToRemove ));
181+ } else {
182+ String noSessionsMessage =
183+ String .format (
184+ "No sessions found to remove from local Session Map for Node %s" , externalUri );
185+ span .addEvent (noSessionsMessage , attributeMap );
186+ LOG .info (noSessionsMessage );
187+ }
188+ }
189+ }
190+
191+ /** Custom ConcurrentMap implementation that automatically maintains a URI-to-SessionId index */
192+ private static class IndexedSessionMap {
193+ private final ConcurrentMap <SessionId , Session > sessions = new ConcurrentHashMap <>();
194+ private final ConcurrentMap <URI , Set <SessionId >> sessionsByUri = new ConcurrentHashMap <>();
195+
196+ public Session get (SessionId id ) {
197+ return sessions .get (id );
198+ }
199+
200+ public Session put (SessionId id , Session session ) {
201+ Session previous = sessions .put (id , session );
202+
203+ if (previous != null && previous .getUri () != null ) {
204+ sessionsByUri .computeIfPresent (
205+ previous .getUri (),
206+ (k , sessionIds ) -> {
207+ sessionIds .remove (id );
208+ return sessionIds .isEmpty () ? null : sessionIds ;
209+ });
210+ }
211+
212+ URI sessionUri = session .getUri ();
213+ if (sessionUri != null ) {
214+ sessionsByUri .computeIfAbsent (sessionUri , k -> ConcurrentHashMap .newKeySet ()).add (id );
215+ }
216+
217+ return previous ;
218+ }
219+
220+ public Session remove (SessionId id ) {
221+ Session removed = sessions .remove (id );
222+
223+ if (removed != null && removed .getUri () != null ) {
224+ sessionsByUri .computeIfPresent (
225+ removed .getUri (),
226+ (k , sessionIds ) -> {
227+ sessionIds .remove (id );
228+ return sessionIds .isEmpty () ? null : sessionIds ;
229+ });
230+ }
231+
232+ return removed ;
233+ }
234+
235+ public void batchRemove (Set <SessionId > sessionIds ) {
236+ Map <URI , Set <SessionId >> uriToSessionIds = new HashMap <>();
237+
238+ for (SessionId id : sessionIds ) {
239+ Session session = sessions .get (id );
240+ if (session != null && session .getUri () != null ) {
241+ uriToSessionIds .computeIfAbsent (session .getUri (), k -> new HashSet <>()).add (id );
242+ }
243+ }
244+
245+ for (SessionId id : sessionIds ) {
246+ sessions .remove (id );
247+ }
248+
249+ for (Map .Entry <URI , Set <SessionId >> entry : uriToSessionIds .entrySet ()) {
250+ URI uri = entry .getKey ();
251+ Set <SessionId > idsToRemove = entry .getValue ();
252+
253+ sessionsByUri .computeIfPresent (
254+ uri ,
255+ (k , existingIds ) -> {
256+ existingIds .removeAll (idsToRemove );
257+ return existingIds .isEmpty () ? null : existingIds ;
258+ });
259+ }
260+ }
261+
262+ public Set <Map .Entry <SessionId , Session >> entrySet () {
263+ return sessions .entrySet ();
264+ }
265+
266+ public Collection <Session > values () {
267+ return sessions .values ();
268+ }
269+
270+ public Set <SessionId > getSessionsByUri (URI uri ) {
271+ return sessionsByUri .getOrDefault (uri , Set .of ());
272+ }
273+
274+ public int size () {
275+ return sessions .size ();
276+ }
277+
278+ public boolean isEmpty () {
279+ return sessions .isEmpty ();
280+ }
281+
282+ public void clear () {
283+ sessions .clear ();
284+ sessionsByUri .clear ();
285+ }
286+ }
138287}
0 commit comments