Skip to content

Commit 0a2f27c

Browse files
committed
Add suggestion to improve code
Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent db33027 commit 0a2f27c

File tree

1 file changed

+145
-82
lines changed

1 file changed

+145
-82
lines changed

java/src/org/openqa/selenium/grid/sessionmap/local/LocalSessionMap.java

Lines changed: 145 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static org.openqa.selenium.remote.RemoteTags.SESSION_ID_EVENT;
2222

2323
import java.net.URI;
24+
import java.util.ArrayList;
2425
import java.util.Collection;
2526
import java.util.HashMap;
2627
import 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

Comments
 (0)