20
20
import static org .openqa .selenium .remote .RemoteTags .SESSION_ID ;
21
21
import static org .openqa .selenium .remote .RemoteTags .SESSION_ID_EVENT ;
22
22
23
- import java .util .List ;
23
+ import java .net .URI ;
24
+ import java .util .Collection ;
25
+ import java .util .Collections ;
26
+ import java .util .HashMap ;
27
+ import java .util .HashSet ;
24
28
import java .util .Map ;
29
+ import java .util .Set ;
25
30
import java .util .concurrent .ConcurrentHashMap ;
26
31
import java .util .concurrent .ConcurrentMap ;
27
32
import java .util .logging .Logger ;
28
- import java .util .stream .Collectors ;
29
33
import org .openqa .selenium .NoSuchSessionException ;
34
+ import org .openqa .selenium .events .Event ;
30
35
import org .openqa .selenium .events .EventBus ;
31
36
import org .openqa .selenium .grid .config .Config ;
32
37
import org .openqa .selenium .grid .data .NodeRemovedEvent ;
@@ -48,7 +53,7 @@ public class LocalSessionMap extends SessionMap {
48
53
private static final Logger LOG = Logger .getLogger (LocalSessionMap .class .getName ());
49
54
50
55
private final EventBus bus ;
51
- private final ConcurrentMap < SessionId , Session > knownSessions = new ConcurrentHashMap <> ();
56
+ private final IndexedSessionMap knownSessions = new IndexedSessionMap ();
52
57
53
58
public LocalSessionMap (Tracer tracer , EventBus bus ) {
54
59
super (tracer );
@@ -59,23 +64,14 @@ public LocalSessionMap(Tracer tracer, EventBus bus) {
59
64
60
65
bus .addListener (
61
66
NodeRemovedEvent .listener (
62
- nodeStatus ->
63
- nodeStatus .getSlots ().stream ()
64
- .filter (slot -> slot .getSession () != null )
65
- .map (slot -> slot .getSession ().getId ())
66
- .forEach (this ::remove )));
67
+ nodeStatus -> {
68
+ batchRemoveByUri (nodeStatus .getExternalUri (), NodeRemovedEvent .class );
69
+ }));
67
70
68
71
bus .addListener (
69
72
NodeRestartedEvent .listener (
70
73
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 );
74
+ batchRemoveByUri (previousNodeStatus .getExternalUri (), NodeRestartedEvent .class );
79
75
}));
80
76
}
81
77
@@ -95,17 +91,23 @@ public boolean isReady() {
95
91
public boolean add (Session session ) {
96
92
Require .nonNull ("Session" , session );
97
93
94
+ SessionId id = session .getId ();
95
+ knownSessions .put (id , session );
96
+
98
97
try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.add" )) {
99
98
AttributeMap attributeMap = tracer .createAttributeMap ();
100
99
attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
101
- SessionId id = session .getId ();
102
100
SESSION_ID .accept (span , id );
103
101
SESSION_ID_EVENT .accept (attributeMap , id );
104
- knownSessions .put (session .getId (), session );
105
- span .addEvent ("Added session into local session map" , attributeMap );
106
102
107
- return true ;
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 );
108
108
}
109
+
110
+ return true ;
109
111
}
110
112
111
113
@ Override
@@ -116,23 +118,157 @@ public Session get(SessionId id) {
116
118
if (session == null ) {
117
119
throw new NoSuchSessionException ("Unable to find session with ID: " + id );
118
120
}
119
-
120
121
return session ;
121
122
}
122
123
123
124
@ Override
124
125
public void remove (SessionId id ) {
125
126
Require .nonNull ("Session ID" , id );
126
127
128
+ Session removedSession = knownSessions .remove (id );
129
+
127
130
try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.remove" )) {
128
131
AttributeMap attributeMap = tracer .createAttributeMap ();
129
132
attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
130
133
SESSION_ID .accept (span , id );
131
134
SESSION_ID_EVENT .accept (attributeMap , id );
132
- knownSessions .remove (id );
133
- String sessionDeletedMessage = "Deleted session from local Session Map" ;
135
+
136
+ String sessionDeletedMessage =
137
+ String .format (
138
+ "Deleted session from local Session Map, Id: %s, Node: %s" ,
139
+ id ,
140
+ removedSession != null ? String .valueOf (removedSession .getUri ()) : "unidentified" );
134
141
span .addEvent (sessionDeletedMessage , attributeMap );
135
- LOG .info (String .format ("%s, Id: %s" , sessionDeletedMessage , id ));
142
+ LOG .info (sessionDeletedMessage );
143
+ }
144
+ }
145
+
146
+ private void batchRemoveByUri (URI externalUri , Class <? extends Event > eventClass ) {
147
+ Set <SessionId > sessionsToRemove = knownSessions .getSessionsByUri (externalUri );
148
+
149
+ if (sessionsToRemove .isEmpty ()) {
150
+ return ; // Early return for empty operations - no tracing overhead
151
+ }
152
+
153
+ knownSessions .batchRemove (sessionsToRemove );
154
+
155
+ try (Span span = tracer .getCurrentContext ().createSpan ("local_sessionmap.batch_remove" )) {
156
+ AttributeMap attributeMap = tracer .createAttributeMap ();
157
+ attributeMap .put (AttributeKey .LOGGER_CLASS .getKey (), getClass ().getName ());
158
+ attributeMap .put ("event.class" , eventClass .getName ());
159
+ attributeMap .put ("node.uri" , externalUri .toString ());
160
+ attributeMap .put ("sessions.count" , sessionsToRemove .size ());
161
+
162
+ String batchRemoveMessage =
163
+ String .format (
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 );
168
+ }
169
+ }
170
+
171
+ private static class IndexedSessionMap {
172
+ private final ConcurrentMap <SessionId , Session > sessions = new ConcurrentHashMap <>();
173
+ private final ConcurrentMap <URI , Set <SessionId >> sessionsByUri = new ConcurrentHashMap <>();
174
+ private final Object coordinationLock = new Object ();
175
+
176
+ public Session get (SessionId id ) {
177
+ return sessions .get (id );
178
+ }
179
+
180
+ public Session put (SessionId id , Session session ) {
181
+ synchronized (coordinationLock ) {
182
+ Session previous = sessions .put (id , session );
183
+
184
+ if (previous != null && previous .getUri () != null ) {
185
+ cleanupUriIndex (previous .getUri (), id );
186
+ }
187
+
188
+ URI sessionUri = session .getUri ();
189
+ if (sessionUri != null ) {
190
+ sessionsByUri .computeIfAbsent (sessionUri , k -> ConcurrentHashMap .newKeySet ()).add (id );
191
+ }
192
+
193
+ return previous ;
194
+ }
195
+ }
196
+
197
+ public Session remove (SessionId id ) {
198
+ synchronized (coordinationLock ) {
199
+ Session removed = sessions .remove (id );
200
+
201
+ if (removed != null && removed .getUri () != null ) {
202
+ cleanupUriIndex (removed .getUri (), id );
203
+ }
204
+
205
+ return removed ;
206
+ }
207
+ }
208
+
209
+ public void batchRemove (Set <SessionId > sessionIds ) {
210
+ synchronized (coordinationLock ) {
211
+ Map <URI , Set <SessionId >> uriToSessionIds = new HashMap <>();
212
+
213
+ // Single loop: remove sessions and collect URI mappings in one pass
214
+ for (SessionId id : sessionIds ) {
215
+ Session session = sessions .remove (id );
216
+ if (session != null && session .getUri () != null ) {
217
+ uriToSessionIds .computeIfAbsent (session .getUri (), k -> new HashSet <>()).add (id );
218
+ }
219
+ }
220
+
221
+ // Clean up URI index for all affected URIs
222
+ for (Map .Entry <URI , Set <SessionId >> entry : uriToSessionIds .entrySet ()) {
223
+ cleanupUriIndex (entry .getKey (), entry .getValue ());
224
+ }
225
+ }
226
+ }
227
+
228
+ private void cleanupUriIndex (URI uri , SessionId sessionId ) {
229
+ sessionsByUri .computeIfPresent (
230
+ uri ,
231
+ (key , sessionIds ) -> {
232
+ sessionIds .remove (sessionId );
233
+ return sessionIds .isEmpty () ? null : sessionIds ;
234
+ });
235
+ }
236
+
237
+ private void cleanupUriIndex (URI uri , Set <SessionId > sessionIdsToRemove ) {
238
+ sessionsByUri .computeIfPresent (
239
+ uri ,
240
+ (key , sessionIds ) -> {
241
+ sessionIds .removeAll (sessionIdsToRemove );
242
+ return sessionIds .isEmpty () ? null : sessionIds ;
243
+ });
244
+ }
245
+
246
+ public Set <SessionId > getSessionsByUri (URI uri ) {
247
+ Set <SessionId > result = sessionsByUri .get (uri );
248
+ return (result != null && !result .isEmpty ()) ? result : Set .of ();
249
+ }
250
+
251
+ public Set <Map .Entry <SessionId , Session >> entrySet () {
252
+ return Collections .unmodifiableSet (sessions .entrySet ());
253
+ }
254
+
255
+ public Collection <Session > values () {
256
+ return Collections .unmodifiableCollection (sessions .values ());
257
+ }
258
+
259
+ public int size () {
260
+ return sessions .size ();
261
+ }
262
+
263
+ public boolean isEmpty () {
264
+ return sessions .isEmpty ();
265
+ }
266
+
267
+ public void clear () {
268
+ synchronized (coordinationLock ) {
269
+ sessions .clear ();
270
+ sessionsByUri .clear ();
271
+ }
136
272
}
137
273
}
138
274
}
0 commit comments