2121import static org .openqa .selenium .remote .RemoteTags .SESSION_ID_EVENT ;
2222
2323import java .net .URI ;
24+ import java .util .ArrayList ;
2425import java .util .Collection ;
2526import java .util .HashMap ;
2627import java .util .HashSet ;
@@ -55,7 +56,6 @@ public class LocalSessionMap extends SessionMap {
5556
5657 private final EventBus bus ;
5758 private final IndexedSessionMap knownSessions = new IndexedSessionMap ();
58- private final ReadWriteLock sessionMapLock = new ReentrantReadWriteLock ();
5959
6060 public LocalSessionMap (Tracer tracer , EventBus bus ) {
6161 super (tracer );
@@ -93,12 +93,9 @@ public boolean isReady() {
9393 public boolean add (Session session ) {
9494 Require .nonNull ("Session" , session );
9595 SessionId id = session .getId ();
96- sessionMapLock .writeLock ().lock ();
97- try {
98- knownSessions .put (id , session );
99- } finally {
100- sessionMapLock .writeLock ().unlock ();
101- }
96+
97+ // IndexedSessionMap now handles internal synchronization
98+ knownSessions .put (id , session );
10299
103100 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.add" )) {
104101 AttributeMap attributeMap = tracer .createAttributeMap ();
@@ -115,29 +112,20 @@ public boolean add(Session session) {
115112 public Session get (SessionId id ) {
116113 Require .nonNull ("Session ID" , id );
117114
118- Session session ;
119- sessionMapLock .readLock ().lock ();
120- try {
121- session = knownSessions .get (id );
122- if (session == null ) {
123- throw new NoSuchSessionException ("Unable to find session with ID: " + id );
124- }
125- } finally {
126- sessionMapLock .readLock ().unlock ();
115+ // IndexedSessionMap now handles internal synchronization
116+ Session session = knownSessions .get (id );
117+ if (session == null ) {
118+ throw new NoSuchSessionException ("Unable to find session with ID: " + id );
127119 }
128120 return session ;
129121 }
130122
131123 @ Override
132124 public void remove (SessionId id ) {
133125 Require .nonNull ("Session ID" , id );
134- Session removedSession ;
135- sessionMapLock .writeLock ().lock ();
136- try {
137- removedSession = knownSessions .remove (id );
138- } finally {
139- sessionMapLock .writeLock ().unlock ();
140- }
126+
127+ // IndexedSessionMap now handles internal synchronization
128+ Session removedSession = knownSessions .remove (id );
141129
142130 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.remove" )) {
143131 AttributeMap attributeMap = tracer .createAttributeMap ();
@@ -156,15 +144,10 @@ public void remove(SessionId id) {
156144
157145 /** Batch remove sessions by URI with proper locking to prevent race conditions */
158146 private void batchRemoveByUri (URI externalUri , Class <? extends Event > eventClass ) {
159- sessionMapLock .writeLock ().lock ();
160- Set <SessionId > sessionsToRemove ;
161- try {
162- sessionsToRemove = knownSessions .getSessionsByUri (externalUri );
163- if (!sessionsToRemove .isEmpty ()) {
164- knownSessions .batchRemove (sessionsToRemove );
165- }
166- } finally {
167- sessionMapLock .writeLock ().unlock ();
147+ // IndexedSessionMap now handles internal synchronization
148+ Set <SessionId > sessionsToRemove = knownSessions .getSessionsByUri (externalUri );
149+ if (!sessionsToRemove .isEmpty ()) {
150+ knownSessions .batchRemove (sessionsToRemove );
168151 }
169152
170153 try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.batch_remove" )) {
@@ -198,83 +181,157 @@ private void batchRemoveByUri(URI externalUri, Class<? extends Event> eventClass
198181 private static class IndexedSessionMap {
199182 private final ConcurrentMap <SessionId , Session > sessions = new ConcurrentHashMap <>();
200183 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 ();
201186
202187 public Session get (SessionId id ) {
188+ // Read operations are atomic on ConcurrentHashMap - no lock needed
203189 return sessions .get (id );
204190 }
205191
206192 public Session put (SessionId id , Session session ) {
207- Session previous = sessions .put (id , session );
208-
209- if (previous != null && previous .getUri () != null ) {
210- sessionsByUri .computeIfPresent (
211- previous .getUri (),
212- (k , sessionIds ) -> {
213- sessionIds .remove (id );
214- return sessionIds .isEmpty () ? null : sessionIds ;
215- });
216- }
193+ // Write lock needed: multiple operations across both maps must be atomic
194+ internalLock .writeLock ().lock ();
195+ try {
196+ Session previous = sessions .put (id , session );
217197
218- URI sessionUri = session .getUri ();
219- if (sessionUri != null ) {
220- sessionsByUri .computeIfAbsent (sessionUri , k -> ConcurrentHashMap .newKeySet ()).add (id );
221- }
198+ if (previous != null && previous .getUri () != null ) {
199+ cleanupUriIndex (previous .getUri (), id );
200+ }
222201
223- return previous ;
202+ URI sessionUri = session .getUri ();
203+ if (sessionUri != null ) {
204+ sessionsByUri .computeIfAbsent (sessionUri , k -> ConcurrentHashMap .newKeySet ()).add (id );
205+ }
206+
207+ return previous ;
208+ } finally {
209+ internalLock .writeLock ().unlock ();
210+ }
224211 }
225212
226213 public Session remove (SessionId id ) {
227- Session removed = sessions .remove (id );
228-
229- if (removed != null && removed .getUri () != null ) {
230- sessionsByUri .computeIfPresent (
231- removed .getUri (),
232- (k , sessionIds ) -> {
233- sessionIds .remove (id );
234- return sessionIds .isEmpty () ? null : sessionIds ;
235- });
236- }
214+ // Write lock needed: multiple operations across both maps must be atomic
215+ internalLock .writeLock ().lock ();
216+ try {
217+ Session removed = sessions .remove (id );
237218
238- return removed ;
219+ if (removed != null && removed .getUri () != null ) {
220+ cleanupUriIndex (removed .getUri (), id );
221+ }
222+
223+ return removed ;
224+ } finally {
225+ internalLock .writeLock ().unlock ();
226+ }
239227 }
240228
241229 public void batchRemove (Set <SessionId > sessionIds ) {
242- Map <URI , Set <SessionId >> uriToSessionIds = new HashMap <>();
230+ // Write lock needed: multiple operations across both maps must be atomic
231+ internalLock .writeLock ().lock ();
232+ try {
233+ Map <URI , Set <SessionId >> uriToSessionIds = new HashMap <>();
234+
235+ for (SessionId id : sessionIds ) {
236+ Session session = sessions .get (id );
237+ if (session != null && session .getUri () != null ) {
238+ uriToSessionIds .computeIfAbsent (session .getUri (), k -> new HashSet <>()).add (id );
239+ }
240+ }
241+
242+ for (SessionId id : sessionIds ) {
243+ sessions .remove (id );
244+ }
245+
246+ // Robust cleanup for each URI
247+ for (Map .Entry <URI , Set <SessionId >> entry : uriToSessionIds .entrySet ()) {
248+ URI uri = entry .getKey ();
249+ Set <SessionId > idsToRemove = entry .getValue ();
250+ cleanupUriIndex (uri , idsToRemove );
251+ }
252+ } finally {
253+ internalLock .writeLock ().unlock ();
254+ }
255+ }
243256
244- for (SessionId id : sessionIds ) {
245- Session session = sessions .get (id );
246- if (session != null && session .getUri () != null ) {
247- uriToSessionIds .computeIfAbsent (session .getUri (), k -> new HashSet <>()).add (id );
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+ */
261+ 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 );
248268 }
249269 }
270+ }
250271
251- for (SessionId id : sessionIds ) {
252- sessions .remove (id );
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+ */
276+ 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+ }
253284 }
285+ }
254286
255- for (Map .Entry <URI , Set <SessionId >> entry : uriToSessionIds .entrySet ()) {
256- URI uri = entry .getKey ();
257- Set <SessionId > idsToRemove = entry .getValue ();
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+ }
258301
259- sessionsByUri .computeIfPresent (
260- uri ,
261- (k , existingIds ) -> {
262- existingIds .removeAll (idsToRemove );
263- return existingIds .isEmpty () ? null : existingIds ;
264- });
302+ for (URI emptyUri : emptyUris ) {
303+ sessionsByUri .remove (emptyUri );
304+ }
305+ } finally {
306+ internalLock .writeLock ().unlock ();
265307 }
266308 }
267309
268- public Set <Map .Entry <SessionId , Session >> entrySet () {
269- return sessions .entrySet ();
310+ public Set <SessionId > getSessionsByUri (URI uri ) {
311+ // Read operations are atomic on ConcurrentHashMap - no lock needed
312+ Set <SessionId > result = sessionsByUri .get (uri );
313+ // Return empty set instead of null, and ensure we don't return empty sets
314+ return (result != null && !result .isEmpty ()) ? result : Set .of ();
270315 }
271316
272- public Collection <Session > values () {
273- return sessions .values ();
317+ 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+ }
274325 }
275326
276- public Set <SessionId > getSessionsByUri (URI uri ) {
277- return sessionsByUri .getOrDefault (uri , Set .of ());
327+ 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+ }
278335 }
279336
280337 public int size () {
@@ -286,8 +343,14 @@ public boolean isEmpty() {
286343 }
287344
288345 public void clear () {
289- sessions .clear ();
290- sessionsByUri .clear ();
346+ // Write lock needed: multiple operations across both maps must be atomic
347+ internalLock .writeLock ().lock ();
348+ try {
349+ sessions .clear ();
350+ sessionsByUri .clear ();
351+ } finally {
352+ internalLock .writeLock ().unlock ();
353+ }
291354 }
292355 }
293356}
0 commit comments