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

com.launchdarkly.eventsource.background.BackgroundEventSource Maven / Gradle / Ivy

There is a newer version: 4.1.1
Show newest version
package com.launchdarkly.eventsource.background;

import com.launchdarkly.eventsource.CommentEvent;
import com.launchdarkly.eventsource.EventSource;
import com.launchdarkly.eventsource.FaultEvent;
import com.launchdarkly.eventsource.MessageEvent;
import com.launchdarkly.eventsource.StartedEvent;
import com.launchdarkly.eventsource.StreamClosedByCallerException;
import com.launchdarkly.eventsource.StreamEvent;
import com.launchdarkly.eventsource.StreamException;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.logging.LogValues;

import java.io.Closeable;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A wrapper for {@link EventSource} that reads the stream on a worker thread,
 * pushing events to a handler that the caller provides.
 * 

* A BackgroundEventSource instance manages two worker threads: one to start the * stream and request events from it, and another to call handler methods. These are * decoupled so that if a handler method is slow to return, it does not completely * block reading of the stream. *

* This is the same asynchronous model that was used by EventSource prior to the * 4.0.0 release. Code that was written against earlier versions of EventSource * can be adapted to use BackgroundEventSource as follows: *


 *     // before (version 3.x)
 *     EventHandler myHandler = new EventHandler() {
 *       public void onMessage(String event, MessageEvent messageEvent) {
 *         // ...
 *       }
 *     };
 *     EventSource es = new EventSource.Builder(uri, myHandler)
 *       .connectTimeout(5, TimeUnit.SECONDS)
 *       .threadPriority(Thread.MAX_PRIORITY)
 *       .build();
 *     es.start();
 *     
 *     // after (version 4.x)
 *     BackgroundEventHandler myHandler = new BackgroundEventHandler() {
 *       public void onMessage(String event, MessageEvent messageEvent) {
 *         // ... these methods are the same as for EventHandler before
 *       }
 *     };
 *     BackgroundEventSource bes = new BackgroundEventSource.Builder(myHandler,
 *         new EventSource.Builder(
 *           ConnectStrategy.http()
 *             .connectTimeout(5, TimeUnit.SECONDS)
 *             // connectTimeout and other HTTP options are now set through
 *             // HttpConnectStrategy
 *           )
 *       )
 *       .threadPriority(Thread.MAX_PRIORITY)
 *         // threadPriority, and other options related to worker threads,
 *         // are now properties of BackgroundEventSource
 *       .build();
 *     bes.start();
 * 
* * @since 4.0.0 */ public class BackgroundEventSource implements Closeable { /** * Default value for {@link Builder#threadBaseName(String)}. */ public static final String DEFAULT_THREAD_BASE_NAME = "EventSource"; private final EventSource eventSource; private final BackgroundEventHandler handler; private final ConnectionErrorHandler connectionErrorHandler; private final Executor streamExecutor; private final Executor eventsExecutor; private final boolean shouldCloseStreamExecutor; private final boolean shouldCloseEventsExecutor; private final Semaphore eventThreadSemaphore; private final AtomicBoolean started = new AtomicBoolean(); private final AtomicBoolean closed = new AtomicBoolean(); private final LDLogger logger; BackgroundEventSource(Builder builder) { this.eventSource = builder.eventSourceBuilder.build(); this.handler = builder.handler; this.connectionErrorHandler = builder.connectionErrorHandler; if (builder.eventsExecutor == null) { this.eventsExecutor = Executors.newSingleThreadExecutor( makeSimpleDaemonThreadFactory("okhttp-eventsource-events", builder.threadBaseName, builder.threadPriority)); this.shouldCloseEventsExecutor = true; } else { this.eventsExecutor = builder.eventsExecutor; this.shouldCloseEventsExecutor = false; } if (builder.streamExecutor == null) { this.streamExecutor = Executors.newSingleThreadExecutor( makeSimpleDaemonThreadFactory("okhttp-eventsource-stream", builder.threadBaseName, builder.threadPriority)); this.shouldCloseStreamExecutor = true; } else { this.streamExecutor = builder.streamExecutor; this.shouldCloseStreamExecutor = false; } if (builder.maxEventTasksInFlight > 0) { eventThreadSemaphore = new Semaphore(builder.maxEventTasksInFlight); } else { eventThreadSemaphore = null; } logger = eventSource.getLogger(); } /** * Starts the worker thread to consume the stream and dispatch events. This also starts * the underlying {@link EventSource} if it has not already been started. *

* This method has no effect if the BackgroundEventSource has already been started, or * if {@link #close()} has been called. */ public void start() { synchronized (this) { if (closed.get() || started.get()) { return; } started.set(true); streamExecutor.execute(new Runnable() { @Override public void run() { logger.debug("BackgroundEventSource started"); while (pollAndDispatchEvent()) {} // If pollAndDispatchEvent returned false, it's time to shut down. We'll spin a // new thread to do the shutdown, so the streamExecutor is not still trying to // run this task. new Thread(new Runnable() { public void run() { close(); } }).start(); } }); } } /** * Returns the underlying EventSource. * * @return the EventSource */ public EventSource getEventSource() { return eventSource; } /** * Shuts down the BackgroundEventSource and the underlying EventSource. *

* The BackgroundEventSource cannot be started again after doing this. */ public void close() { synchronized (this) { if (closed.getAndSet(true)) { return; } } logger.debug("BackgroundEventSource stopping"); eventSource.close(); if (shouldCloseStreamExecutor && streamExecutor instanceof ExecutorService) { ((ExecutorService)streamExecutor).shutdown(); try { ((ExecutorService)streamExecutor).awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) {} } if (shouldCloseEventsExecutor && eventsExecutor instanceof ExecutorService) { ((ExecutorService)eventsExecutor).shutdown(); try { ((ExecutorService)eventsExecutor).awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) {} } } private boolean pollAndDispatchEvent() { StreamEvent event; try { event = eventSource.readAnyEvent(); } catch (StreamException e) { event = new FaultEvent(e); } if (event instanceof FaultEvent) { StreamException e = ((FaultEvent)event).getCause(); if (e instanceof StreamClosedByCallerException) { return false; // don't bother dispatching anything, just quit } dispatchEvent(event); if (connectionErrorHandler != null) { if (connectionErrorHandler.onConnectionError(e) == ConnectionErrorHandler.Action.SHUTDOWN) { return false; } } } else { dispatchEvent(event); } return true; } private void dispatchEvent(StreamEvent event) { if (closed.get()) { return; // COVERAGE: this condition can't be reproduced in unit tests } if (eventThreadSemaphore != null) { try { eventThreadSemaphore.acquire(); } catch (InterruptedException e) { // COVERAGE: this condition can't be reproduced in unit tests throw new RejectedExecutionException("Thread interrupted while waiting for event thread semaphore", e); } } eventsExecutor.execute(new Runnable() { @Override public void run() { try { if (event instanceof MessageEvent) { MessageEvent me = (MessageEvent)event; try { handler.onMessage(me.getEventName(), me); } finally { me.close(); } } else if (event instanceof CommentEvent) { CommentEvent ce = (CommentEvent)event; handler.onComment(ce.getText()); } else if (event instanceof StartedEvent) { handler.onOpen(); } else if (event instanceof FaultEvent) { FaultEvent se = (FaultEvent)event; if (!(se.getCause() instanceof StreamClosedByCallerException)) { handler.onError(se.getCause()); } handler.onClosed(); } } catch (Exception e) { logger.warn("Caught unexpected error from EventHandler: {}", LogValues.exceptionSummary(e)); logger.debug(LogValues.exceptionTrace(e)); try { handler.onError(e); } catch (Exception ee) { logger.warn("Caught unexpected error from EventHandler.onError(): {}", LogValues.exceptionSummary(ee)); logger.debug(LogValues.exceptionTrace(ee)); } } finally { if (eventThreadSemaphore != null) { eventThreadSemaphore.release(); } } } }); } private ThreadFactory makeSimpleDaemonThreadFactory( String categoryName, String threadBaseName, final int threadPriority ) { final String baseName = categoryName + "[" + threadBaseName + "]"; final ThreadGroup threadGroup = new ThreadGroup(baseName); final AtomicInteger counter = new AtomicInteger(0); threadGroup.setDaemon(true); return new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(threadGroup, r, baseName + "-" + counter.incrementAndGet()); t.setDaemon(true); if (threadPriority > 0) { t.setPriority(threadPriority); } return t; } }; } /** * Builder for configuring {@link BackgroundEventSource}. */ public static class Builder { private final EventSource.Builder eventSourceBuilder; private final BackgroundEventHandler handler; private ConnectionErrorHandler connectionErrorHandler; private Executor eventsExecutor; private int maxEventTasksInFlight; private Executor streamExecutor; private String threadBaseName; private int threadPriority; /** * Creates a builder. * * @param handler the event handler * @param eventSourceBuilder builder for the underlying EventSource */ public Builder(BackgroundEventHandler handler, EventSource.Builder eventSourceBuilder) { if (handler == null) { throw new IllegalArgumentException("handler cannot be null"); } if (eventSourceBuilder == null) { throw new IllegalArgumentException("eventSourceBuilder cannot be null"); } this.eventSourceBuilder = eventSourceBuilder; this.handler = handler; } /** * Creates a {@link BackgroundEventSource} with this configuration. *

* The stream is not started until you call {@link BackgroundEventSource#start()}. * * @return the configured {@link BackgroundEventSource} */ public BackgroundEventSource build() { return new BackgroundEventSource(this); } /** * Sets the {@link ConnectionErrorHandler} that should process connection errors. * * @param handler the error handler * @return the builder */ public Builder connectionErrorHandler(ConnectionErrorHandler handler) { this.connectionErrorHandler = handler; return this; } /** * Specifies a custom executor to use for dispatching events. *

* If you do not specify a custom executor, the default is to call * {@link Executors#newSingleThreadExecutor(java.util.concurrent.ThreadFactory)}, using * a simple thread factory that creates daemon threads whose properties are based on * {@link #threadBaseName(String)} and {@link #threadPriority(Integer)}. This executor * will be shut down when you close the BackgroundEventSource. *

* If you do specify a custom executor, it will not be shut down when you close * the BackgroundEventSource; you are responsible for its lifecycle. * * @param eventsExecutor an executor, or null to use the default behavior * @return the builder */ public Builder eventsExecutor(Executor eventsExecutor) { this.eventsExecutor = eventsExecutor; return this; } /** * Specifies the maximum number of tasks that can be "in-flight" for the thread executing * {@link BackgroundEventHandler}. A semaphore will be used to artificially constrain the * number of tasks sitting in the queue fronting the event handler thread. When this limit * is reached the stream thread will block until the backpressure passes. *

* The default is no maximum. * * @param maxEventTasksInFlight the maximum number of tasks/messages that can be in-flight * for the {@code BackgorundEventHandler} * @return the builder */ public Builder maxEventTasksInFlight(int maxEventTasksInFlight) { this.maxEventTasksInFlight = maxEventTasksInFlight; return this; } /** * Specifies a custom executor to use for running the stream-reading task. *

* If you do not specify a custom executor, the default is to call * {@link Executors#newSingleThreadExecutor(java.util.concurrent.ThreadFactory)}, using * a simple thread factory that creates daemon threads whose properties are based on * {@link #threadBaseName(String)} and {@link #threadPriority(Integer)}. This executor * will be shut down when you close the BackgroundEventSource. *

* If you do specify a custom executor, it will not be shut down when you close * the BackgroundEventSource; you are responsible for its lifecycle. * * @param streamExecutor an executor, or null to use the default behavior * @return the builder */ public Builder streamExecutor(Executor streamExecutor) { this.streamExecutor = streamExecutor; return this; } /** * Set the name for this BackgroundEventSource to be used when naming thread pools, when * BackgroundEventSource is using its default executors. *

* This is mainly useful when multiple BackgroundEventSource instances exist within the same process. *

* This setting is ignored if you have specified a custom executor with * {@link #eventsExecutor(Executor)} or {@link #streamExecutor(Executor)}. * * @param threadBaseName a string to be used in worker thread names (must not contain spaces) * @return the builder */ public Builder threadBaseName(String threadBaseName) { this.threadBaseName = threadBaseName == null ? DEFAULT_THREAD_BASE_NAME : threadBaseName; return this; } /** * Specifies the priority for threads created by BackgroundEventSource using its default * executors. *

* If this is left unset, or set to {@code null}, threads will inherit the default priority * provided by {@code Executors.defaultThreadFactory()}. *

* This setting is ignored if you have specified a custom executor with * {@link #eventsExecutor(Executor)} or {@link #streamExecutor(Executor)}. * * @param threadPriority the thread priority, or null to use the default * @return the builder */ public Builder threadPriority(Integer threadPriority) { this.threadPriority = threadPriority == null ? 0 : threadPriority.intValue(); return this; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy