48
48
import java .util .concurrent .ConcurrentHashMap ;
49
49
import java .util .concurrent .ExecutorService ;
50
50
import java .util .concurrent .Executors ;
51
+ import java .util .concurrent .ScheduledExecutorService ;
52
+ import java .util .concurrent .ThreadFactory ;
53
+ import java .util .concurrent .TimeUnit ;
51
54
import java .util .concurrent .atomic .AtomicBoolean ;
52
55
import java .util .function .Consumer ;
53
56
import java .util .logging .Level ;
@@ -60,7 +63,9 @@ class UnboundZmqEventBus implements EventBus {
60
63
static final EventName REJECTED_EVENT = new EventName ("selenium-rejected-event" );
61
64
private static final Logger LOG = Logger .getLogger (EventBus .class .getName ());
62
65
private static final Json JSON = new Json ();
63
- private final ExecutorService executor ;
66
+ private final ScheduledExecutorService socketPollingExecutor ;
67
+ private final ExecutorService listenerNotificationExecutor ;
68
+
64
69
private final Map <EventName , List <Consumer <Event >>> listeners = new ConcurrentHashMap <>();
65
70
private final Queue <UUID > recentMessages = EvictingQueue .create (128 );
66
71
private final String encodedSecret ;
@@ -76,12 +81,16 @@ class UnboundZmqEventBus implements EventBus {
76
81
}
77
82
this .encodedSecret = builder .toString ();
78
83
79
- executor = Executors . newSingleThreadExecutor ( r -> {
84
+ ThreadFactory threadFactory = r -> {
80
85
Thread thread = new Thread (r );
81
86
thread .setName ("Event Bus" );
82
87
thread .setDaemon (true );
83
88
return thread ;
84
- });
89
+ };
90
+ this .socketPollingExecutor = Executors .newSingleThreadScheduledExecutor (threadFactory );
91
+ this .listenerNotificationExecutor = Executors .newFixedThreadPool (
92
+ Math .max (Runtime .getRuntime ().availableProcessors () / 2 , 2 ), // At least two threads
93
+ threadFactory );
85
94
86
95
String connectionMessage = String .format ("Connecting to %s and %s" , publishConnection , subscribeConnection );
87
96
LOG .info (connectionMessage );
@@ -93,6 +102,7 @@ class UnboundZmqEventBus implements EventBus {
93
102
.onRetry (e -> LOG .log (Level .WARNING , String .format ("Failure #%s. Retrying." , e .getAttemptCount ())))
94
103
.onRetriesExceeded (e -> LOG .log (Level .WARNING , "Connection aborted." ));
95
104
105
+ // Access to the zmq socket is safe here: no threads.
96
106
Failsafe .with (retryPolicy ).run (
97
107
() -> {
98
108
sub = context .createSocket (SocketType .SUB );
@@ -113,54 +123,11 @@ class UnboundZmqEventBus implements EventBus {
113
123
114
124
AtomicBoolean pollingStarted = new AtomicBoolean (false );
115
125
116
- executor .submit (() -> {
117
- LOG .info ("Bus started" );
118
- while (!Thread .currentThread ().isInterrupted ()) {
119
- try {
120
- poller .poll (150 );
121
- pollingStarted .lazySet (true );
122
-
123
- if (poller .pollin (0 )) {
124
- ZMQ .Socket socket = poller .getSocket (0 );
125
-
126
- EventName eventName = new EventName (new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 ));
127
- Secret eventSecret = JSON .toType (new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 ), Secret .class );
128
- UUID id = UUID .fromString (new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 ));
129
- String data = new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 );
130
-
131
- Object converted = JSON .toType (data , Object .class );
132
- Event event = new Event (id , eventName , converted );
133
-
134
- if (recentMessages .contains (id )) {
135
- continue ;
136
- }
137
- recentMessages .add (id );
138
-
139
- if (!Secret .matches (secret , eventSecret )) {
140
- LOG .severe (String .format ("Received message without a valid secret. Rejecting. %s -> %s" , event , data ));
141
- Event rejectedEvent = new Event (REJECTED_EVENT , new ZeroMqEventBus .RejectedEvent (eventName , data ));
142
-
143
- listeners .getOrDefault (REJECTED_EVENT , new ArrayList <>())
144
- .forEach (listener -> listener .accept (rejectedEvent ));
145
- return ;
146
- }
147
-
148
- List <Consumer <Event >> typeListeners = listeners .get (eventName );
149
- if (typeListeners == null ) {
150
- continue ;
151
- }
152
-
153
- typeListeners .parallelStream ().forEach (listener -> listener .accept (event ));
154
- }
155
- } catch (Throwable e ) {
156
- if (e .getCause () != null && e .getCause () instanceof AssertionError ) {
157
- // Do nothing.
158
- } else {
159
- throw e ;
160
- }
161
- }
162
- }
163
- });
126
+ socketPollingExecutor .scheduleWithFixedDelay (
127
+ () -> pollForIncomingEvents (poller , secret , pollingStarted ),
128
+ 0 ,
129
+ 100 ,
130
+ TimeUnit .MILLISECONDS );
164
131
165
132
// Give ourselves up to a second to connect, using The World's Worst heuristic. If we don't
166
133
// manage to connect, it's not the end of the world, as the socket we're connecting to may not
@@ -173,11 +140,13 @@ class UnboundZmqEventBus implements EventBus {
173
140
throw new RuntimeException (e );
174
141
}
175
142
}
143
+
144
+ LOG .info ("Event bus ready" );
176
145
}
177
146
178
147
@ Override
179
148
public boolean isReady () {
180
- return !executor .isShutdown ();
149
+ return !socketPollingExecutor .isShutdown ();
181
150
}
182
151
183
152
private boolean isSubAddressIPv6 (String connection ) {
@@ -207,20 +176,83 @@ public void addListener(EventListener<?> listener) {
207
176
public void fire (Event event ) {
208
177
Require .nonNull ("Event to send" , event );
209
178
210
- pub .sendMore (event .getType ().getName ().getBytes (UTF_8 ));
211
- pub .sendMore (encodedSecret .getBytes (UTF_8 ));
212
- pub .sendMore (event .getId ().toString ().getBytes (UTF_8 ));
213
- pub .send (event .getRawData ().getBytes (UTF_8 ));
179
+ socketPollingExecutor .execute (() -> {
180
+ pub .sendMore (event .getType ().getName ().getBytes (UTF_8 ));
181
+ pub .sendMore (encodedSecret .getBytes (UTF_8 ));
182
+ pub .sendMore (event .getId ().toString ().getBytes (UTF_8 ));
183
+ pub .send (event .getRawData ().getBytes (UTF_8 ));
184
+ });
214
185
}
215
186
216
187
@ Override
217
188
public void close () {
218
- executor .shutdown ();
189
+ socketPollingExecutor .shutdownNow ();
190
+ listenerNotificationExecutor .shutdownNow ();
191
+
219
192
if (sub != null ) {
220
193
sub .close ();
221
194
}
222
195
if (pub != null ) {
223
196
pub .close ();
224
197
}
225
198
}
199
+
200
+ private void pollForIncomingEvents (ZMQ .Poller poller , Secret secret , AtomicBoolean pollingStarted ) {
201
+ try {
202
+ int count = poller .poll (0 );
203
+
204
+ pollingStarted .lazySet (true );
205
+
206
+ for (int i = 0 ; i < count ; i ++) {
207
+ if (poller .pollin (i )) {
208
+ ZMQ .Socket socket = poller .getSocket (i );
209
+
210
+ EventName eventName = new EventName (new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 ));
211
+ Secret eventSecret =
212
+ JSON .toType (new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 ), Secret .class );
213
+ UUID id = UUID .fromString (new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 ));
214
+ String data = new String (socket .recv (ZMQ .DONTWAIT ), UTF_8 );
215
+
216
+ Object converted = JSON .toType (data , Object .class );
217
+ Event event = new Event (id , eventName , converted );
218
+
219
+ if (recentMessages .contains (id )) {
220
+ return ;
221
+ }
222
+ recentMessages .add (id );
223
+
224
+ if (!Secret .matches (secret , eventSecret )) {
225
+ LOG .severe (
226
+ String .format (
227
+ "Received message without a valid secret. Rejecting. %s -> %s" , event , data ));
228
+ Event rejectedEvent =
229
+ new Event (REJECTED_EVENT , new ZeroMqEventBus .RejectedEvent (eventName , data ));
230
+
231
+ notifyListeners (REJECTED_EVENT , rejectedEvent );
232
+
233
+ return ;
234
+ }
235
+
236
+ notifyListeners (eventName , event );
237
+ }
238
+ }
239
+ } catch (Throwable e ) {
240
+ // if the exception escapes, then we never get scheduled again
241
+ LOG .log (Level .WARNING , e , () -> "Caught and swallowed exception: " + e .getMessage ());
242
+ }
243
+ }
244
+
245
+ private void notifyListeners (EventName eventName , Event event ) {
246
+ List <Consumer <Event >> eventListeners = listeners .getOrDefault (eventName , new ArrayList <>());
247
+ eventListeners .stream ().forEach (listener -> {
248
+ listenerNotificationExecutor .submit (() -> {
249
+ try {
250
+ listener .accept (event );
251
+
252
+ } catch (Throwable t ) {
253
+ LOG .log (Level .WARNING , t , () -> "Caught exception from listener: " + listener );
254
+ }
255
+ });
256
+ });
257
+ }
226
258
}
0 commit comments