2121import static org .openqa .selenium .remote .RemoteTags .SESSION_ID_EVENT ;
2222
2323import java .net .URI ;
24- import java .util .ArrayList ;
2524import java .util .Collection ;
25+ import java .util .Collections ;
2626import java .util .HashMap ;
2727import java .util .HashSet ;
2828import java .util .Map ;
2929import java .util .Set ;
3030import java .util .concurrent .ConcurrentHashMap ;
3131import java .util .concurrent .ConcurrentMap ;
32- import java .util .concurrent .locks .ReadWriteLock ;
33- import java .util .concurrent .locks .ReentrantReadWriteLock ;
3432import java .util .logging .Logger ;
3533import org .openqa .selenium .NoSuchSessionException ;
3634import org .openqa .selenium .events .Event ;
@@ -92,17 +90,21 @@ public boolean isReady() {
9290 @ Override
9391 public boolean add (Session session ) {
9492 Require .nonNull ("Session" , session );
95- SessionId id = session .getId ();
9693
97- // IndexedSessionMap now handles internal synchronization
94+ SessionId id = session . getId ();
9895 knownSessions .put (id , session );
9996
10097 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.add" )) {
10198 AttributeMap attributeMap = tracer .createAttributeMap ();
10299 attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
103100 SESSION_ID .accept (span , id );
104101 SESSION_ID_EVENT .accept (attributeMap , id );
105- span .addEvent ("Added session into local Session Map" , attributeMap );
102+
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 );
106108 }
107109
108110 return true ;
@@ -112,7 +114,6 @@ public boolean add(Session session) {
112114 public Session get (SessionId id ) {
113115 Require .nonNull ("Session ID" , id );
114116
115- // IndexedSessionMap now handles internal synchronization
116117 Session session = knownSessions .get (id );
117118 if (session == null ) {
118119 throw new NoSuchSessionException ("Unable to find session with ID: " + id );
@@ -124,14 +125,14 @@ public Session get(SessionId id) {
124125 public void remove (SessionId id ) {
125126 Require .nonNull ("Session ID" , id );
126127
127- // IndexedSessionMap now handles internal synchronization
128128 Session removedSession = knownSessions .remove (id );
129129
130130 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.remove" )) {
131131 AttributeMap attributeMap = tracer .createAttributeMap ();
132132 attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
133133 SESSION_ID .accept (span , id );
134134 SESSION_ID_EVENT .accept (attributeMap , id );
135+
135136 String sessionDeletedMessage =
136137 String .format (
137138 "Deleted session from local Session Map, Id: %s, Node: %s" ,
@@ -142,57 +143,42 @@ public void remove(SessionId id) {
142143 }
143144 }
144145
145- /** Batch remove sessions by URI with proper locking to prevent race conditions */
146146 private void batchRemoveByUri (URI externalUri , Class <? extends Event > eventClass ) {
147- // IndexedSessionMap now handles internal synchronization
148147 Set <SessionId > sessionsToRemove = knownSessions .getSessionsByUri (externalUri );
149- if (!sessionsToRemove .isEmpty ()) {
150- knownSessions .batchRemove (sessionsToRemove );
148+
149+ if (sessionsToRemove .isEmpty ()) {
150+ return ; // Early return for empty operations - no tracing overhead
151151 }
152152
153+ knownSessions .batchRemove (sessionsToRemove );
154+
153155 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.batch_remove" )) {
154156 AttributeMap attributeMap = tracer .createAttributeMap ();
155157 attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
156158 attributeMap .put ("event.class" , eventClass .getName ());
157159 attributeMap .put ("node.uri" , externalUri .toString ());
158160 attributeMap .put ("sessions.count" , sessionsToRemove .size ());
159161
160- LOG . info (
162+ String batchRemoveMessage =
161163 String .format (
162- "Event %s triggered batch remove from local Session Map for Node %s" ,
163- eventClass .getName (), externalUri ));
164- String eventMessage = "" ;
165- if (!sessionsToRemove .isEmpty ()) {
166- eventMessage =
167- String .format (
168- "Batch removed %d sessions belonging to Node %s including: %s" ,
169- sessionsToRemove .size (), externalUri , sessionsToRemove );
170- } else {
171- eventMessage =
172- String .format (
173- "No sessions found to remove from local Session Map for Node %s" , externalUri );
174- }
175- span .addEvent (eventMessage , attributeMap );
176- LOG .info (eventMessage );
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 );
177168 }
178169 }
179170
180- /** Custom ConcurrentMap implementation that automatically maintains a URI-to-SessionId index */
181171 private static class IndexedSessionMap {
182172 private final ConcurrentMap <SessionId , Session > sessions = new ConcurrentHashMap <>();
183173 private final ConcurrentMap <URI , Set <SessionId >> sessionsByUri = new ConcurrentHashMap <>();
184- // Internal lock to ensure atomicity of multi-step operations across both maps
185- private final ReadWriteLock internalLock = new ReentrantReadWriteLock ();
174+ private final Object coordinationLock = new Object ();
186175
187176 public Session get (SessionId id ) {
188- // Read operations are atomic on ConcurrentHashMap - no lock needed
189177 return sessions .get (id );
190178 }
191179
192180 public Session put (SessionId id , Session session ) {
193- // Write lock needed: multiple operations across both maps must be atomic
194- internalLock .writeLock ().lock ();
195- try {
181+ synchronized (coordinationLock ) {
196182 Session previous = sessions .put (id , session );
197183
198184 if (previous != null && previous .getUri () != null ) {
@@ -205,133 +191,69 @@ public Session put(SessionId id, Session session) {
205191 }
206192
207193 return previous ;
208- } finally {
209- internalLock .writeLock ().unlock ();
210194 }
211195 }
212196
213197 public Session remove (SessionId id ) {
214- // Write lock needed: multiple operations across both maps must be atomic
215- internalLock .writeLock ().lock ();
216- try {
198+ synchronized (coordinationLock ) {
217199 Session removed = sessions .remove (id );
218200
219201 if (removed != null && removed .getUri () != null ) {
220202 cleanupUriIndex (removed .getUri (), id );
221203 }
222204
223205 return removed ;
224- } finally {
225- internalLock .writeLock ().unlock ();
226206 }
227207 }
228208
229209 public void batchRemove (Set <SessionId > sessionIds ) {
230- // Write lock needed: multiple operations across both maps must be atomic
231- internalLock .writeLock ().lock ();
232- try {
210+ synchronized (coordinationLock ) {
233211 Map <URI , Set <SessionId >> uriToSessionIds = new HashMap <>();
234212
213+ // Single loop: remove sessions and collect URI mappings in one pass
235214 for (SessionId id : sessionIds ) {
236- Session session = sessions .get (id );
215+ Session session = sessions .remove (id );
237216 if (session != null && session .getUri () != null ) {
238217 uriToSessionIds .computeIfAbsent (session .getUri (), k -> new HashSet <>()).add (id );
239218 }
240219 }
241220
242- for (SessionId id : sessionIds ) {
243- sessions .remove (id );
244- }
245-
246- // Robust cleanup for each URI
221+ // Clean up URI index for all affected URIs
247222 for (Map .Entry <URI , Set <SessionId >> entry : uriToSessionIds .entrySet ()) {
248- URI uri = entry .getKey ();
249- Set <SessionId > idsToRemove = entry .getValue ();
250- cleanupUriIndex (uri , idsToRemove );
223+ cleanupUriIndex (entry .getKey (), entry .getValue ());
251224 }
252- } finally {
253- internalLock .writeLock ().unlock ();
254225 }
255226 }
256227
257- /**
258- * Robust cleanup of URI index to prevent memory leaks from empty sets. Handles single session
259- * removal with explicit empty set cleanup.
260- */
261228 private void cleanupUriIndex (URI uri , SessionId sessionId ) {
262- Set <SessionId > sessionIds = sessionsByUri .get (uri );
263- if (sessionIds != null ) {
264- sessionIds .remove (sessionId );
265- // Explicit check and removal to prevent memory leaks
266- if (sessionIds .isEmpty ()) {
267- sessionsByUri .remove (uri , sessionIds );
268- }
269- }
229+ sessionsByUri .computeIfPresent (
230+ uri ,
231+ (key , sessionIds ) -> {
232+ sessionIds .remove (sessionId );
233+ return sessionIds .isEmpty () ? null : sessionIds ;
234+ });
270235 }
271236
272- /**
273- * Robust cleanup of URI index to prevent memory leaks from empty sets. Handles batch session
274- * removal with explicit empty set cleanup.
275- */
276237 private void cleanupUriIndex (URI uri , Set <SessionId > sessionIdsToRemove ) {
277- Set <SessionId > sessionIds = sessionsByUri .get (uri );
278- if (sessionIds != null ) {
279- sessionIds .removeAll (sessionIdsToRemove );
280- // Explicit check and removal to prevent memory leaks
281- if (sessionIds .isEmpty ()) {
282- sessionsByUri .remove (uri , sessionIds );
283- }
284- }
285- }
286-
287- /**
288- * Periodic cleanup to remove any empty sets that may have been missed. Should be called
289- * periodically to prevent memory leaks.
290- */
291- public void performMaintenanceCleanup () {
292- internalLock .writeLock ().lock ();
293- try {
294- // Find and remove empty URI sets
295- Set <URI > emptyUris = new HashSet <>();
296- for (Map .Entry <URI , Set <SessionId >> entry : sessionsByUri .entrySet ()) {
297- if (entry .getValue ().isEmpty ()) {
298- emptyUris .add (entry .getKey ());
299- }
300- }
301-
302- for (URI emptyUri : emptyUris ) {
303- sessionsByUri .remove (emptyUri );
304- }
305- } finally {
306- internalLock .writeLock ().unlock ();
307- }
238+ sessionsByUri .computeIfPresent (
239+ uri ,
240+ (key , sessionIds ) -> {
241+ sessionIds .removeAll (sessionIdsToRemove );
242+ return sessionIds .isEmpty () ? null : sessionIds ;
243+ });
308244 }
309245
310246 public Set <SessionId > getSessionsByUri (URI uri ) {
311- // Read operations are atomic on ConcurrentHashMap - no lock needed
312247 Set <SessionId > result = sessionsByUri .get (uri );
313- // Return empty set instead of null, and ensure we don't return empty sets
314248 return (result != null && !result .isEmpty ()) ? result : Set .of ();
315249 }
316250
317251 public Set <Map .Entry <SessionId , Session >> entrySet () {
318- // Read lock to ensure consistent view during iteration
319- internalLock .readLock ().lock ();
320- try {
321- return new HashSet <>(sessions .entrySet ());
322- } finally {
323- internalLock .readLock ().unlock ();
324- }
252+ return Collections .unmodifiableSet (sessions .entrySet ());
325253 }
326254
327255 public Collection <Session > values () {
328- // Read lock to ensure consistent view during iteration
329- internalLock .readLock ().lock ();
330- try {
331- return new ArrayList <>(sessions .values ());
332- } finally {
333- internalLock .readLock ().unlock ();
334- }
256+ return Collections .unmodifiableCollection (sessions .values ());
335257 }
336258
337259 public int size () {
@@ -343,13 +265,9 @@ public boolean isEmpty() {
343265 }
344266
345267 public void clear () {
346- // Write lock needed: multiple operations across both maps must be atomic
347- internalLock .writeLock ().lock ();
348- try {
268+ synchronized (coordinationLock ) {
349269 sessions .clear ();
350270 sessionsByUri .clear ();
351- } finally {
352- internalLock .writeLock ().unlock ();
353271 }
354272 }
355273 }
0 commit comments