1818
1919import com .optimizely .ab .HttpClientUtils ;
2020import com .optimizely .ab .NamedThreadFactory ;
21+ import com .optimizely .ab .annotations .VisibleForTesting ;
2122
2223import org .apache .http .HttpResponse ;
2324import org .apache .http .client .ClientProtocolException ;
3334import org .slf4j .Logger ;
3435import org .slf4j .LoggerFactory ;
3536
36- import java .io .Closeable ;
3737import java .io .IOException ;
3838import java .io .UnsupportedEncodingException ;
3939import java .net .URISyntaxException ;
4040import java .util .Map ;
4141import java .util .concurrent .ArrayBlockingQueue ;
42- import java .util .concurrent .BlockingQueue ;
4342import 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 ;
4546
4647import javax .annotation .CheckForNull ;
4748
4849/**
4950 * {@link EventHandler} implementation that queues events and has a separate pool of threads responsible
5051 * for the dispatch.
5152 */
52- public class AsyncEventHandler implements EventHandler , Closeable {
53+ public class AsyncEventHandler implements EventHandler {
5354
5455 // The following static values are public so that they can be tweaked if necessary.
5556 // 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 {
6566
6667 private final CloseableHttpClient httpClient ;
6768 private final ExecutorService workerExecutor ;
68- private final BlockingQueue <LogEvent > logEventQueue ;
6969
7070 public AsyncEventHandler (int queueCapacity , int numWorkers ) {
7171 this (queueCapacity , numWorkers , 200 , 20 , 5000 );
@@ -80,21 +80,22 @@ public AsyncEventHandler(int queueCapacity, int numWorkers, int maxConnections,
8080 this .maxPerRoute = connectionsPerRoute ;
8181 this .validateAfterInactivity = validateAfter ;
8282
83- this .logEventQueue = new ArrayBlockingQueue <LogEvent >(queueCapacity );
84- this .httpClient = HttpClients .custom ()
83+ this .httpClient = HttpClients .custom ()
8584 .setDefaultRequestConfig (HttpClientUtils .DEFAULT_REQUEST_CONFIG )
8685 .setConnectionManager (poolingHttpClientConnectionManager ())
8786 .disableCookieManagement ()
8887 .build ();
8988
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+ }
9294
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 ;
9899 }
99100
100101 private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager ()
@@ -108,61 +109,87 @@ private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager()
108109
109110 @ Override
110111 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" );
115117 }
116118 }
117119
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 ();
121134
122- // "close" all workers and the http client
123135 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
128150 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+ }
129159 }
160+
161+ logger .info ("event handler shutdown complete" );
130162 }
131163
132164 //======== Helper classes ========//
133165
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+ }
135176
136177 @ Override
137178 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 );
159186 }
160187 }
161188
162189 /**
163190 * Helper method that generates the event request for the given {@link LogEvent}.
164191 */
165- private HttpGet generateGetRequest (LogEvent event ) throws URISyntaxException {
192+ private HttpGet generateRequest (LogEvent event ) throws URISyntaxException {
166193
167194 URIBuilder builder = new URIBuilder (event .getEndpointUrl ());
168195 for (Map .Entry <String , String > param : event .getRequestParams ().entrySet ()) {
@@ -171,13 +198,6 @@ private HttpGet generateGetRequest(LogEvent event) throws URISyntaxException {
171198
172199 return new HttpGet (builder .build ());
173200 }
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- }
181201 }
182202
183203 /**
0 commit comments