Skip to content

Commit 6f98efc

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

File tree

1 file changed

+139
-88
lines changed

1 file changed

+139
-88
lines changed

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

Lines changed: 139 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
import java.util.Set;
3030
import java.util.concurrent.ConcurrentHashMap;
3131
import java.util.concurrent.ConcurrentMap;
32-
import java.util.concurrent.locks.ReadWriteLock;
33-
import java.util.concurrent.locks.ReentrantReadWriteLock;
32+
import java.util.concurrent.Executors;
33+
import java.util.concurrent.ScheduledExecutorService;
34+
import java.util.concurrent.TimeUnit;
3435
import java.util.logging.Logger;
3536
import org.openqa.selenium.NoSuchSessionException;
3637
import org.openqa.selenium.events.Event;
@@ -50,18 +51,35 @@
5051
import org.openqa.selenium.remote.tracing.Span;
5152
import org.openqa.selenium.remote.tracing.Tracer;
5253

53-
public class LocalSessionMap extends SessionMap {
54+
public class LocalSessionMap extends SessionMap implements AutoCloseable {
5455

5556
private static final Logger LOG = Logger.getLogger(LocalSessionMap.class.getName());
5657

5758
private final EventBus bus;
5859
private final IndexedSessionMap knownSessions = new IndexedSessionMap();
60+
private final ScheduledExecutorService maintenanceExecutor;
5961

6062
public LocalSessionMap(Tracer tracer, EventBus bus) {
6163
super(tracer);
6264

6365
this.bus = Require.nonNull("Event bus", bus);
6466

67+
// Initialize maintenance executor for periodic cleanup
68+
this.maintenanceExecutor =
69+
Executors.newSingleThreadScheduledExecutor(
70+
r -> {
71+
Thread t = new Thread(r, "LocalSessionMap-Maintenance");
72+
t.setDaemon(true);
73+
return t;
74+
});
75+
76+
// Schedule maintenance cleanup every 5 minutes to prevent memory leaks
77+
this.maintenanceExecutor.scheduleWithFixedDelay(
78+
this::performMaintenanceCleanup,
79+
5, // initial delay
80+
5, // period
81+
TimeUnit.MINUTES);
82+
6583
bus.addListener(SessionClosedEvent.listener(this::remove));
6684

6785
bus.addListener(
@@ -92,17 +110,21 @@ public boolean isReady() {
92110
@Override
93111
public boolean add(Session session) {
94112
Require.nonNull("Session", session);
95-
SessionId id = session.getId();
96113

97-
// IndexedSessionMap now handles internal synchronization
114+
SessionId id = session.getId();
98115
knownSessions.put(id, session);
99116

100117
try (Span span = tracer.getCurrentContext().createSpan("local_sessionmap.add")) {
101118
AttributeMap attributeMap = tracer.createAttributeMap();
102119
attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(), getClass().getName());
103120
SESSION_ID.accept(span, id);
104121
SESSION_ID_EVENT.accept(attributeMap, id);
105-
span.addEvent("Added session into local Session Map", attributeMap);
122+
123+
String sessionAddedMessage =
124+
String.format(
125+
"Added session to local Session Map, Id: %s, Node: %s", id, session.getUri());
126+
span.addEvent(sessionAddedMessage, attributeMap);
127+
LOG.info(sessionAddedMessage);
106128
}
107129

108130
return true;
@@ -132,6 +154,7 @@ public void remove(SessionId id) {
132154
attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(), getClass().getName());
133155
SESSION_ID.accept(span, id);
134156
SESSION_ID_EVENT.accept(attributeMap, id);
157+
135158
String sessionDeletedMessage =
136159
String.format(
137160
"Deleted session from local Session Map, Id: %s, Node: %s",
@@ -157,199 +180,227 @@ private void batchRemoveByUri(URI externalUri, Class<? extends Event> eventClass
157180
attributeMap.put("node.uri", externalUri.toString());
158181
attributeMap.put("sessions.count", sessionsToRemove.size());
159182

160-
LOG.info(
161-
String.format(
162-
"Event %s triggered batch remove from local Session Map for Node %s",
163-
eventClass.getName(), externalUri));
164-
String eventMessage = "";
165183
if (!sessionsToRemove.isEmpty()) {
166-
eventMessage =
184+
String batchRemoveMessage =
167185
String.format(
168-
"Batch removed %d sessions belonging to Node %s including: %s",
169-
sessionsToRemove.size(), externalUri, sessionsToRemove);
186+
"Batch removed %d sessions from local Session Map for Node %s (triggered by %s):"
187+
+ " %s",
188+
sessionsToRemove.size(), externalUri, eventClass.getSimpleName(), sessionsToRemove);
189+
span.addEvent(batchRemoveMessage, attributeMap);
190+
LOG.info(batchRemoveMessage);
170191
} else {
171-
eventMessage =
192+
String noSessionsMessage =
172193
String.format(
173-
"No sessions found to remove from local Session Map for Node %s", externalUri);
194+
"No sessions found to remove from local Session Map for Node %s (triggered by %s)",
195+
externalUri, eventClass.getSimpleName());
196+
span.addEvent(noSessionsMessage, attributeMap);
197+
}
198+
}
199+
}
200+
201+
private void performMaintenanceCleanup() {
202+
knownSessions.performMaintenanceCleanup();
203+
}
204+
205+
/**
206+
* Shutdown the maintenance executor and perform final cleanup. This method should be called when
207+
* the LocalSessionMap is no longer needed.
208+
*/
209+
@Override
210+
public void close() {
211+
try {
212+
// Perform final maintenance cleanup before shutdown
213+
performMaintenanceCleanup();
214+
215+
// Shutdown the maintenance executor gracefully
216+
maintenanceExecutor.shutdown();
217+
if (!maintenanceExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
218+
LOG.warning("Maintenance executor did not terminate gracefully, forcing shutdown");
219+
maintenanceExecutor.shutdownNow();
174220
}
175-
span.addEvent(eventMessage, attributeMap);
176-
LOG.info(eventMessage);
221+
222+
LOG.info("LocalSessionMap maintenance executor shutdown completed");
223+
} catch (InterruptedException e) {
224+
Thread.currentThread().interrupt();
225+
maintenanceExecutor.shutdownNow();
226+
LOG.warning("Interrupted during maintenance executor shutdown");
227+
} catch (Exception e) {
228+
LOG.warning("Error during LocalSessionMap shutdown: " + e.getMessage());
177229
}
178230
}
179231

180232
/** Custom ConcurrentMap implementation that automatically maintains a URI-to-SessionId index */
181233
private static class IndexedSessionMap {
182234
private final ConcurrentMap<SessionId, Session> sessions = new ConcurrentHashMap<>();
183235
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();
236+
// Simplified lock strategy: only for operations requiring coordination between both maps
237+
private final Object coordinationLock = new Object();
186238

187239
public Session get(SessionId id) {
188-
// Read operations are atomic on ConcurrentHashMap - no lock needed
240+
// ConcurrentHashMap is already thread-safe for reads - no lock needed
189241
return sessions.get(id);
190242
}
191243

192244
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 {
245+
// Synchronize only the coordination between maps, not individual map operations
246+
synchronized (coordinationLock) {
196247
Session previous = sessions.put(id, session);
197248

249+
// Clean up previous session's URI index if it exists
198250
if (previous != null && previous.getUri() != null) {
199251
cleanupUriIndex(previous.getUri(), id);
200252
}
201253

254+
// Add new session to URI index
202255
URI sessionUri = session.getUri();
203256
if (sessionUri != null) {
204257
sessionsByUri.computeIfAbsent(sessionUri, k -> ConcurrentHashMap.newKeySet()).add(id);
205258
}
206259

207260
return previous;
208-
} finally {
209-
internalLock.writeLock().unlock();
210261
}
211262
}
212263

213264
public Session remove(SessionId id) {
214-
// Write lock needed: multiple operations across both maps must be atomic
215-
internalLock.writeLock().lock();
216-
try {
265+
// Synchronize only the coordination between maps
266+
synchronized (coordinationLock) {
217267
Session removed = sessions.remove(id);
218268

269+
// Clean up URI index if session existed
219270
if (removed != null && removed.getUri() != null) {
220271
cleanupUriIndex(removed.getUri(), id);
221272
}
222273

223274
return removed;
224-
} finally {
225-
internalLock.writeLock().unlock();
226275
}
227276
}
228277

229278
public void batchRemove(Set<SessionId> sessionIds) {
230-
// Write lock needed: multiple operations across both maps must be atomic
231-
internalLock.writeLock().lock();
232-
try {
279+
// Synchronize the entire batch operation for consistency
280+
synchronized (coordinationLock) {
233281
Map<URI, Set<SessionId>> uriToSessionIds = new HashMap<>();
234282

283+
// Collect URI mappings for sessions that exist
235284
for (SessionId id : sessionIds) {
236285
Session session = sessions.get(id);
237286
if (session != null && session.getUri() != null) {
238287
uriToSessionIds.computeIfAbsent(session.getUri(), k -> new HashSet<>()).add(id);
239288
}
240289
}
241290

291+
// Remove all sessions from main map
242292
for (SessionId id : sessionIds) {
243293
sessions.remove(id);
244294
}
245295

246-
// Robust cleanup for each URI
296+
// Clean up URI index for all affected URIs
247297
for (Map.Entry<URI, Set<SessionId>> entry : uriToSessionIds.entrySet()) {
248298
URI uri = entry.getKey();
249299
Set<SessionId> idsToRemove = entry.getValue();
250300
cleanupUriIndex(uri, idsToRemove);
251301
}
252-
} finally {
253-
internalLock.writeLock().unlock();
254302
}
255303
}
256304

257305
/**
258306
* Robust cleanup of URI index to prevent memory leaks from empty sets. Handles single session
259-
* removal with explicit empty set cleanup.
307+
* removal with explicit empty set cleanup. Uses atomic operations to prevent race conditions.
308+
* Note: This method should only be called from within coordinationLock synchronized blocks.
260309
*/
261310
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-
}
311+
// Use computeIfPresent for atomic operation to prevent race conditions
312+
sessionsByUri.computeIfPresent(
313+
uri,
314+
(key, sessionIds) -> {
315+
sessionIds.remove(sessionId);
316+
// Return null to remove the entry if empty, otherwise return the modified set
317+
return sessionIds.isEmpty() ? null : sessionIds;
318+
});
270319
}
271320

272321
/**
273322
* Robust cleanup of URI index to prevent memory leaks from empty sets. Handles batch session
274-
* removal with explicit empty set cleanup.
323+
* removal with explicit empty set cleanup. Uses atomic operations to prevent race conditions.
324+
* Note: This method should only be called from within coordinationLock synchronized blocks.
275325
*/
276326
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-
}
327+
// Use computeIfPresent for atomic operation to prevent race conditions
328+
sessionsByUri.computeIfPresent(
329+
uri,
330+
(key, sessionIds) -> {
331+
sessionIds.removeAll(sessionIdsToRemove);
332+
// Return null to remove the entry if empty, otherwise return the modified set
333+
return sessionIds.isEmpty() ? null : sessionIds;
334+
});
285335
}
286336

287337
/**
288338
* Periodic cleanup to remove any empty sets that may have been missed. Should be called
289-
* periodically to prevent memory leaks.
339+
* periodically to prevent memory leaks. Enhanced with better error handling and logging.
290340
*/
291341
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());
342+
synchronized (coordinationLock) {
343+
try {
344+
int initialSize = sessionsByUri.size();
345+
int emptyUrisRemoved = 0;
346+
347+
// Use iterator to safely remove empty entries during iteration
348+
var iterator = sessionsByUri.entrySet().iterator();
349+
while (iterator.hasNext()) {
350+
var entry = iterator.next();
351+
if (entry.getValue().isEmpty()) {
352+
iterator.remove();
353+
emptyUrisRemoved++;
354+
}
299355
}
300-
}
301356

302-
for (URI emptyUri : emptyUris) {
303-
sessionsByUri.remove(emptyUri);
357+
if (emptyUrisRemoved > 0) {
358+
LOG.info(
359+
String.format(
360+
"Maintenance cleanup removed %d empty URI entries from sessionsByUri map (size:"
361+
+ " %d -> %d)",
362+
emptyUrisRemoved, initialSize, sessionsByUri.size()));
363+
}
364+
} catch (Exception e) {
365+
LOG.warning("Error during maintenance cleanup: " + e.getMessage());
304366
}
305-
} finally {
306-
internalLock.writeLock().unlock();
307367
}
308368
}
309369

310370
public Set<SessionId> getSessionsByUri(URI uri) {
311-
// Read operations are atomic on ConcurrentHashMap - no lock needed
371+
// ConcurrentHashMap is already thread-safe for reads - no lock needed
312372
Set<SessionId> result = sessionsByUri.get(uri);
313373
// Return empty set instead of null, and ensure we don't return empty sets
314374
return (result != null && !result.isEmpty()) ? result : Set.of();
315375
}
316376

317377
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-
}
378+
// ConcurrentHashMap provides thread-safe iteration - no lock needed
379+
// Create defensive copy to prevent external modification
380+
return new HashSet<>(sessions.entrySet());
325381
}
326382

327383
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-
}
384+
// ConcurrentHashMap provides thread-safe iteration - no lock needed
385+
// Create defensive copy to prevent external modification
386+
return new ArrayList<>(sessions.values());
335387
}
336388

337389
public int size() {
390+
// ConcurrentHashMap size() is already thread-safe
338391
return sessions.size();
339392
}
340393

341394
public boolean isEmpty() {
395+
// ConcurrentHashMap isEmpty() is already thread-safe
342396
return sessions.isEmpty();
343397
}
344398

345399
public void clear() {
346-
// Write lock needed: multiple operations across both maps must be atomic
347-
internalLock.writeLock().lock();
348-
try {
400+
// Synchronize clearing both maps for consistency
401+
synchronized (coordinationLock) {
349402
sessions.clear();
350403
sessionsByUri.clear();
351-
} finally {
352-
internalLock.writeLock().unlock();
353404
}
354405
}
355406
}

0 commit comments

Comments
 (0)