18
18
19
19
import com .optimizely .ab .HttpClientUtils ;
20
20
import com .optimizely .ab .NamedThreadFactory ;
21
+ import com .optimizely .ab .annotations .VisibleForTesting ;
21
22
22
23
import org .apache .http .HttpResponse ;
23
24
import org .apache .http .client .ClientProtocolException ;
33
34
import org .slf4j .Logger ;
34
35
import org .slf4j .LoggerFactory ;
35
36
36
- import java .io .Closeable ;
37
37
import java .io .IOException ;
38
38
import java .io .UnsupportedEncodingException ;
39
39
import java .net .URISyntaxException ;
40
40
import java .util .Map ;
41
41
import java .util .concurrent .ArrayBlockingQueue ;
42
- import java .util .concurrent .BlockingQueue ;
43
42
import java .util .concurrent .ExecutorService ;
44
- import java .util .concurrent .Executors ;
43
+ import java .util .concurrent .RejectedExecutionException ;
44
+ import java .util .concurrent .ThreadPoolExecutor ;
45
+ import java .util .concurrent .TimeUnit ;
45
46
46
47
import javax .annotation .CheckForNull ;
47
48
48
49
/**
49
50
* {@link EventHandler} implementation that queues events and has a separate pool of threads responsible
50
51
* for the dispatch.
51
52
*/
52
- public class AsyncEventHandler implements EventHandler , Closeable {
53
+ public class AsyncEventHandler implements EventHandler {
53
54
54
55
// The following static values are public so that they can be tweaked if necessary.
55
56
// These are the recommended settings for http protocol. https://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html
@@ -65,7 +66,6 @@ public class AsyncEventHandler implements EventHandler, Closeable {
65
66
66
67
private final CloseableHttpClient httpClient ;
67
68
private final ExecutorService workerExecutor ;
68
- private final BlockingQueue <LogEvent > logEventQueue ;
69
69
70
70
public AsyncEventHandler (int queueCapacity , int numWorkers ) {
71
71
this (queueCapacity , numWorkers , 200 , 20 , 5000 );
@@ -80,21 +80,22 @@ public AsyncEventHandler(int queueCapacity, int numWorkers, int maxConnections,
80
80
this .maxPerRoute = connectionsPerRoute ;
81
81
this .validateAfterInactivity = validateAfter ;
82
82
83
- this .logEventQueue = new ArrayBlockingQueue <LogEvent >(queueCapacity );
84
- this .httpClient = HttpClients .custom ()
83
+ this .httpClient = HttpClients .custom ()
85
84
.setDefaultRequestConfig (HttpClientUtils .DEFAULT_REQUEST_CONFIG )
86
85
.setConnectionManager (poolingHttpClientConnectionManager ())
87
86
.disableCookieManagement ()
88
87
.build ();
89
88
90
- this .workerExecutor = Executors .newFixedThreadPool (
91
- numWorkers , new NamedThreadFactory ("optimizely-event-dispatcher-thread-%s" , true ));
89
+ this .workerExecutor = new ThreadPoolExecutor (numWorkers , numWorkers ,
90
+ 0L , TimeUnit .MILLISECONDS ,
91
+ new ArrayBlockingQueue <Runnable >(queueCapacity ),
92
+ new NamedThreadFactory ("optimizely-event-dispatcher-thread-%s" , true ));
93
+ }
92
94
93
- // create dispatch workers
94
- for (int i = 0 ; i < numWorkers ; i ++) {
95
- EventDispatchWorker worker = new EventDispatchWorker ();
96
- workerExecutor .submit (worker );
97
- }
95
+ @ VisibleForTesting
96
+ public AsyncEventHandler (CloseableHttpClient httpClient , ExecutorService workerExecutor ) {
97
+ this .httpClient = httpClient ;
98
+ this .workerExecutor = workerExecutor ;
98
99
}
99
100
100
101
private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager ()
@@ -108,61 +109,87 @@ private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager()
108
109
109
110
@ Override
110
111
public void dispatchEvent (LogEvent logEvent ) {
111
- // attempt to enqueue the log event for processing
112
- boolean submitted = logEventQueue .offer (logEvent );
113
- if (!submitted ) {
114
- logger .error ("unable to enqueue event because queue is full" );
112
+ try {
113
+ // attempt to enqueue the log event for processing
114
+ workerExecutor .execute (new EventDispatcher (logEvent ));
115
+ } catch (RejectedExecutionException e ) {
116
+ logger .error ("event dispatch rejected" );
115
117
}
116
118
}
117
119
118
- @ Override
119
- public void close () throws IOException {
120
- logger .info ("closing event dispatcher" );
120
+ /**
121
+ * Attempts to gracefully terminate all event dispatch workers and close all resources.
122
+ * This method blocks, awaiting the completion of any queued or ongoing event dispatches.
123
+ *
124
+ * Note: termination of ongoing event dispatching is best-effort.
125
+ *
126
+ * @param timeout maximum time to wait for event dispatches to complete
127
+ * @param unit the time unit of the timeout argument
128
+ */
129
+ public void shutdownAndAwaitTermination (long timeout , TimeUnit unit ) {
130
+
131
+ // Disable new tasks from being submitted
132
+ logger .info ("event handler shutting down. Attempting to dispatch previously submitted events" );
133
+ workerExecutor .shutdown ();
121
134
122
- // "close" all workers and the http client
123
135
try {
124
- httpClient .close ();
125
- } catch (IOException e ) {
126
- logger .error ("unable to close the event handler httpclient cleanly" , e );
127
- } finally {
136
+ // Wait a while for existing tasks to terminate
137
+ if (!workerExecutor .awaitTermination (timeout , unit )) {
138
+ int unprocessedCount = workerExecutor .shutdownNow ().size ();
139
+ logger .warn ("timed out waiting for previously submitted events to be dispatched. "
140
+ + "{} events were dropped. "
141
+ + "Interrupting dispatch worker(s)" , unprocessedCount );
142
+ // Cancel currently executing tasks
143
+ // Wait a while for tasks to respond to being cancelled
144
+ if (!workerExecutor .awaitTermination (timeout , unit )) {
145
+ logger .error ("unable to gracefully shutdown event handler" );
146
+ }
147
+ }
148
+ } catch (InterruptedException ie ) {
149
+ // (Re-)Cancel if current thread also interrupted
128
150
workerExecutor .shutdownNow ();
151
+ // Preserve interrupt status
152
+ Thread .currentThread ().interrupt ();
153
+ } finally {
154
+ try {
155
+ httpClient .close ();
156
+ } catch (IOException e ) {
157
+ logger .error ("unable to close event dispatcher http client" , e );
158
+ }
129
159
}
160
+
161
+ logger .info ("event handler shutdown complete" );
130
162
}
131
163
132
164
//======== Helper classes ========//
133
165
134
- private class EventDispatchWorker implements Runnable {
166
+ /**
167
+ * Wrapper runnable for the actual event dispatch.
168
+ */
169
+ private class EventDispatcher implements Runnable {
170
+
171
+ private final LogEvent logEvent ;
172
+
173
+ EventDispatcher (LogEvent logEvent ) {
174
+ this .logEvent = logEvent ;
175
+ }
135
176
136
177
@ Override
137
178
public void run () {
138
- boolean terminate = false ;
139
-
140
- logger .info ("starting event dispatch worker" );
141
- // event loop that'll block waiting for events to appear in the queue
142
- //noinspection InfiniteLoopStatement
143
- while (!terminate ) {
144
- try {
145
- LogEvent event = logEventQueue .take ();
146
- HttpRequestBase request ;
147
- if (event .getRequestMethod () == LogEvent .RequestMethod .GET ) {
148
- request = generateGetRequest (event );
149
- } else {
150
- request = generatePostRequest (event );
151
- }
152
- httpClient .execute (request , EVENT_RESPONSE_HANDLER );
153
- } catch (InterruptedException e ) {
154
- logger .info ("terminating event dispatcher event loop" );
155
- terminate = true ;
156
- } catch (Throwable t ) {
157
- logger .error ("event dispatcher threw exception but will continue" , t );
158
- }
179
+ try {
180
+ HttpGet request = generateRequest (logEvent );
181
+ httpClient .execute (request , EVENT_RESPONSE_HANDLER );
182
+ } catch (IOException e ) {
183
+ logger .error ("event dispatch failed" , e );
184
+ } catch (URISyntaxException e ) {
185
+ logger .error ("unable to parse generated URI" , e );
159
186
}
160
187
}
161
188
162
189
/**
163
190
* Helper method that generates the event request for the given {@link LogEvent}.
164
191
*/
165
- private HttpGet generateGetRequest (LogEvent event ) throws URISyntaxException {
192
+ private HttpGet generateRequest (LogEvent event ) throws URISyntaxException {
166
193
167
194
URIBuilder builder = new URIBuilder (event .getEndpointUrl ());
168
195
for (Map .Entry <String , String > param : event .getRequestParams ().entrySet ()) {
@@ -171,13 +198,6 @@ private HttpGet generateGetRequest(LogEvent event) throws URISyntaxException {
171
198
172
199
return new HttpGet (builder .build ());
173
200
}
174
-
175
- private HttpPost generatePostRequest (LogEvent event ) throws UnsupportedEncodingException {
176
- HttpPost post = new HttpPost (event .getEndpointUrl ());
177
- post .setEntity (new StringEntity (event .getBody ()));
178
- post .addHeader ("Content-Type" , "application/json" );
179
- return post ;
180
- }
181
201
}
182
202
183
203
/**
0 commit comments