All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.optimizely.ab.event.AsyncEventHandler Maven / Gradle / Ivy

Go to download

The Java SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts

There is a newer version: 4.1.1
Show newest version
/**
 *
 *    Copyright 2016-2019,2021, Optimizely and contributors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.optimizely.ab.event;

import com.optimizely.ab.NamedThreadFactory;
import com.optimizely.ab.OptimizelyHttpClient;
import com.optimizely.ab.annotations.VisibleForTesting;

import com.optimizely.ab.internal.PropertyUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.annotation.CheckForNull;

/**
 * {@link EventHandler} implementation that queues events and has a separate pool of threads responsible
 * for the dispatch.
 */
public class AsyncEventHandler implements EventHandler, AutoCloseable {

    public static final String CONFIG_QUEUE_CAPACITY            = "async.event.handler.queue.capacity";
    public static final String CONFIG_NUM_WORKERS               = "async.event.handler.num.workers";
    public static final String CONFIG_MAX_CONNECTIONS           = "async.event.handler.max.connections";
    public static final String CONFIG_MAX_PER_ROUTE             = "async.event.handler.event.max.per.route";
    public static final String CONFIG_VALIDATE_AFTER_INACTIVITY = "async.event.handler.validate.after";

    public static final int DEFAULT_QUEUE_CAPACITY = 10000;
    public static final int DEFAULT_NUM_WORKERS = 2;
    public static final int DEFAULT_MAX_CONNECTIONS = 200;
    public static final int DEFAULT_MAX_PER_ROUTE = 20;
    public static final int DEFAULT_VALIDATE_AFTER_INACTIVITY = 5000;

    private static final Logger logger = LoggerFactory.getLogger(AsyncEventHandler.class);
    private static final ProjectConfigResponseHandler EVENT_RESPONSE_HANDLER = new ProjectConfigResponseHandler();

    private final OptimizelyHttpClient httpClient;
    private final ExecutorService workerExecutor;

    private final long closeTimeout;
    private final TimeUnit closeTimeoutUnit;

    /**
     * @deprecated Use the builder {@link Builder}
     *
     * @param queueCapacity     A depth of the event queue
     * @param numWorkers        The number of workers
     */
    @Deprecated
    public AsyncEventHandler(int queueCapacity,
                             int numWorkers) {
        this(queueCapacity, numWorkers, 200, 20, 5000);
    }

    /**
     * @deprecated Use the builder {@link Builder}
     *
     * @param queueCapacity     A depth of the event queue
     * @param numWorkers        The number of workers
     * @param maxConnections    The max number of concurrent connections
     * @param connectionsPerRoute  The max number of concurrent connections per route
     * @param validateAfter     An inactivity period in milliseconds after which persistent connections must be re-validated prior to being leased to the consumer.
     */
    @Deprecated
    public AsyncEventHandler(int queueCapacity,
                             int numWorkers,
                             int maxConnections,
                             int connectionsPerRoute,
                             int validateAfter) {
        this(queueCapacity, numWorkers, maxConnections, connectionsPerRoute, validateAfter, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
    }

    public AsyncEventHandler(int queueCapacity,
                             int numWorkers,
                             int maxConnections,
                             int connectionsPerRoute,
                             int validateAfter,
                             long closeTimeout,
                             TimeUnit closeTimeoutUnit) {

        queueCapacity       = validateInput("queueCapacity", queueCapacity, DEFAULT_QUEUE_CAPACITY);
        numWorkers          = validateInput("numWorkers", numWorkers, DEFAULT_NUM_WORKERS);
        maxConnections      = validateInput("maxConnections", maxConnections, DEFAULT_MAX_CONNECTIONS);
        connectionsPerRoute = validateInput("connectionsPerRoute", connectionsPerRoute, DEFAULT_MAX_PER_ROUTE);
        validateAfter       = validateInput("validateAfter", validateAfter, DEFAULT_VALIDATE_AFTER_INACTIVITY);

        this.httpClient = OptimizelyHttpClient.builder()
            .withMaxTotalConnections(maxConnections)
            .withMaxPerRoute(connectionsPerRoute)
            .withValidateAfterInactivity(validateAfter)
            // infrequent event discards observed. staled connections force-closed after a long idle time.
            .withEvictIdleConnections(1L, TimeUnit.MINUTES)
            .build();

        this.workerExecutor = new ThreadPoolExecutor(numWorkers, numWorkers,
            0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(queueCapacity),
            new NamedThreadFactory("optimizely-event-dispatcher-thread-%s", true));

        this.closeTimeout = closeTimeout;
        this.closeTimeoutUnit = closeTimeoutUnit;
    }

    @VisibleForTesting
    public AsyncEventHandler(OptimizelyHttpClient httpClient, ExecutorService workerExecutor) {
        this.httpClient = httpClient;
        this.workerExecutor = workerExecutor;
        this.closeTimeout = Long.MAX_VALUE;
        this.closeTimeoutUnit = TimeUnit.MILLISECONDS;
    }

    @Override
    public void dispatchEvent(LogEvent logEvent) {
        try {
            // attempt to enqueue the log event for processing
            workerExecutor.execute(new EventDispatcher(logEvent));
        } catch (RejectedExecutionException e) {
            logger.error("event dispatch rejected");
        }
    }

    /**
     * Attempts to gracefully terminate all event dispatch workers and close all resources.
     * This method blocks, awaiting the completion of any queued or ongoing event dispatches.
     * 

* Note: termination of ongoing event dispatching is best-effort. * * @param timeout maximum time to wait for event dispatches to complete * @param unit the time unit of the timeout argument */ public void shutdownAndAwaitTermination(long timeout, TimeUnit unit) { // Disable new tasks from being submitted logger.info("event handler shutting down. Attempting to dispatch previously submitted events"); workerExecutor.shutdown(); try { // Wait a while for existing tasks to terminate if (!workerExecutor.awaitTermination(timeout, unit)) { int unprocessedCount = workerExecutor.shutdownNow().size(); logger.warn("timed out waiting for previously submitted events to be dispatched. " + "{} events were dropped. " + "Interrupting dispatch worker(s)", unprocessedCount); // Cancel currently executing tasks // Wait a while for tasks to respond to being cancelled if (!workerExecutor.awaitTermination(timeout, unit)) { logger.error("unable to gracefully shutdown event handler"); } } } catch (InterruptedException ie) { // (Re-)Cancel if current thread also interrupted workerExecutor.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } finally { try { httpClient.close(); } catch (IOException e) { logger.error("unable to close event dispatcher http client", e); } } logger.info("event handler shutdown complete"); } @Override public void close() { shutdownAndAwaitTermination(closeTimeout, closeTimeoutUnit); } //======== Helper classes ========// /** * Wrapper runnable for the actual event dispatch. */ private class EventDispatcher implements Runnable { private final LogEvent logEvent; EventDispatcher(LogEvent logEvent) { this.logEvent = logEvent; } @Override public void run() { if (logger.isDebugEnabled()) { logger.debug("Dispatching event to URL {} with params {} and payload \"{}\".", logEvent.getEndpointUrl(), logEvent.getRequestParams(), logEvent.getBody()); } try { HttpRequestBase request; if (logEvent.getRequestMethod() == LogEvent.RequestMethod.GET) { request = generateGetRequest(logEvent); } else { request = generatePostRequest(logEvent); } httpClient.execute(request, EVENT_RESPONSE_HANDLER); } catch (IOException e) { logger.error("event dispatch failed", e); } catch (URISyntaxException e) { logger.error("unable to parse generated URI", e); } } /** * Helper method that generates the event request for the given {@link LogEvent}. */ private HttpGet generateGetRequest(LogEvent event) throws URISyntaxException { URIBuilder builder = new URIBuilder(event.getEndpointUrl()); for (Map.Entry param : event.getRequestParams().entrySet()) { builder.addParameter(param.getKey(), param.getValue()); } return new HttpGet(builder.build()); } private HttpPost generatePostRequest(LogEvent event) throws UnsupportedEncodingException { HttpPost post = new HttpPost(event.getEndpointUrl()); post.setEntity(new StringEntity(event.getBody())); post.addHeader("Content-Type", "application/json"); return post; } } /** * Handler for the event request. */ private static final class ProjectConfigResponseHandler implements ResponseHandler { @Override @CheckForNull public Void handleResponse(HttpResponse response) throws IOException { int status = response.getStatusLine().getStatusCode(); if (status >= 200 && status < 300) { // read the response, so we can close the connection response.getEntity(); return null; } else { throw new ClientProtocolException("unexpected response from event endpoint, status: " + status); } } } //======== Builder ========// public static Builder builder() { return new Builder(); } public static class Builder { int queueCapacity = PropertyUtils.getInteger(CONFIG_QUEUE_CAPACITY, DEFAULT_QUEUE_CAPACITY); int numWorkers = PropertyUtils.getInteger(CONFIG_NUM_WORKERS, DEFAULT_NUM_WORKERS); int maxTotalConnections = PropertyUtils.getInteger(CONFIG_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS); int maxPerRoute = PropertyUtils.getInteger(CONFIG_MAX_PER_ROUTE, DEFAULT_MAX_PER_ROUTE); int validateAfterInactivity = PropertyUtils.getInteger(CONFIG_VALIDATE_AFTER_INACTIVITY, DEFAULT_VALIDATE_AFTER_INACTIVITY); private long closeTimeout = Long.MAX_VALUE; private TimeUnit closeTimeoutUnit = TimeUnit.MILLISECONDS; public Builder withQueueCapacity(int queueCapacity) { if (queueCapacity <= 0) { logger.warn("Queue capacity cannot be <= 0. Keeping default value: {}", this.queueCapacity); return this; } this.queueCapacity = queueCapacity; return this; } public Builder withNumWorkers(int numWorkers) { if (numWorkers <= 0) { logger.warn("Number of workers cannot be <= 0. Keeping default value: {}", this.numWorkers); return this; } this.numWorkers = numWorkers; return this; } public Builder withMaxTotalConnections(int maxTotalConnections) { this.maxTotalConnections = maxTotalConnections; return this; } public Builder withMaxPerRoute(int maxPerRoute) { this.maxPerRoute = maxPerRoute; return this; } public Builder withValidateAfterInactivity(int validateAfterInactivity) { this.validateAfterInactivity = validateAfterInactivity; return this; } public Builder withCloseTimeout(long closeTimeout, TimeUnit unit) { this.closeTimeout = closeTimeout; this.closeTimeoutUnit = unit; return this; } public AsyncEventHandler build() { return new AsyncEventHandler( queueCapacity, numWorkers, maxTotalConnections, maxPerRoute, validateAfterInactivity, closeTimeout, closeTimeoutUnit ); } } private int validateInput(String name, int input, int fallback) { if (input <= 0) { logger.warn("Invalid value for {}: {}. Defaulting to {}", name, input, fallback); return fallback; } return input; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy