From a0aa42b690fabe4fb85060e9e2789f11047d052b Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 17 Aug 2015 14:47:42 -0700 Subject: [PATCH 01/15] Initial support for streaming mode --- build.gradle | 3 +- .../com/launchdarkly/client/EventSource.java | 664 ++++++++++++++++++ .../com/launchdarkly/client/FeatureRep.java | 16 + .../com/launchdarkly/client/FeatureStore.java | 15 + .../client/InMemoryFeatureStore.java | 97 +++ .../com/launchdarkly/client/LDClient.java | 110 +-- .../com/launchdarkly/client/LDConfig.java | 27 + .../launchdarkly/client/StreamProcessor.java | 123 ++++ 8 files changed, 1014 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/launchdarkly/client/EventSource.java create mode 100644 src/main/java/com/launchdarkly/client/FeatureStore.java create mode 100644 src/main/java/com/launchdarkly/client/InMemoryFeatureStore.java create mode 100644 src/main/java/com/launchdarkly/client/StreamProcessor.java diff --git a/build.gradle b/build.gradle index 8c3f07667..8b614f25c 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ repositories { allprojects { group = 'com.launchdarkly' - version = "0.10.0" + version = "0.11.0" sourceCompatibility = 1.6 targetCompatibility = 1.6 } @@ -22,6 +22,7 @@ dependencies { compile "commons-codec:commons-codec:1.5" compile "com.google.code.gson:gson:2.2.4" compile "org.slf4j:slf4j-api:1.7.7" + compile "org.glassfish.jersey.media:jersey-media-sse:2.20" testCompile "org.easymock:easymock:3.3" testCompile 'junit:junit:[4.10,)' testRuntime "org.slf4j:slf4j-simple:1.7.7" diff --git a/src/main/java/com/launchdarkly/client/EventSource.java b/src/main/java/com/launchdarkly/client/EventSource.java new file mode 100644 index 000000000..63fed2d54 --- /dev/null +++ b/src/main/java/com/launchdarkly/client/EventSource.java @@ -0,0 +1,664 @@ +package com.launchdarkly.client; + +import org.glassfish.jersey.internal.util.collection.StringKeyIgnoreCaseMultivaluedMap; +import org.glassfish.jersey.media.sse.*; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.ServiceUnavailableException; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MultivaluedMap; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; + +// EventSource class modified from +// https://github.com/jersey/jersey/blob/master/media/sse/src/main/java/org/glassfish/jersey/media/sse/EventSource.java +// Modifications: +// - support for custom headers +// - set spawned thread as a daemon to permit application shutdown +public class EventSource implements EventListener { + + /** + * Default SSE {@link EventSource} reconnect delay value in milliseconds. + * + * @since 2.3 + */ + public static final long RECONNECT_DEFAULT = 500; + + private static enum State { + READY, OPEN, CLOSED + } + + private static final Level CONNECTION_ERROR_LEVEL = Level.FINE; + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(EventSource.class); + + /** + * SSE streaming resource target. + */ + private final WebTarget target; + /** + * Default reconnect delay. + */ + private final long reconnectDelay; + /** + * Flag indicating if the persistent HTTP connections should be disabled. + */ + private final boolean disableKeepAlive; + /** + * Incoming SSE event processing task executor. + */ + private final ScheduledExecutorService executor; + /** + * Event source internal state. + */ + private final AtomicReference state = new AtomicReference(State.READY); + /** + * List of all listeners not bound to receive only events of a particular name. + */ + private final List unboundListeners = new CopyOnWriteArrayList(); + /** + * A map of listeners bound to receive only events of a particular name. + */ + private final ConcurrentMap> boundListeners = new ConcurrentHashMap>(); + + private final MultivaluedMap headers; + + /** + * Jersey {@link EventSource} builder class. + * + * Event source builder provides methods that let you conveniently configure and subsequently build + * a new {@code EventSource} instance. You can obtain a new event source builder instance using + * a static {@link EventSource#target(javax.ws.rs.client.WebTarget) EventSource.target(endpoint)} factory method. + * + * For example: + * + * EventSource es = EventSource.target(endpoint).named("my source") + * .reconnectingEvery(5, SECONDS) + * .open(); + * + *
+ * For example: + *
+ * EventSource es = EventSource.target(endpoint).named("my source") + * .reconnectingEvery(5, SECONDS) + * .open(); + *
+ * At present, custom event source name is mainly useful to be able to distinguish different event source + * event processing threads from one another. If not set, a default name will be generated using the + * SSE endpoint URI. + *
+ * By default, the persistent HTTP connections are disabled for the reasons discussed in the {@link EventSource} + * javadoc. + *
+ * Note that this value may be later overridden by the SSE endpoint using either a {@code retry} SSE event field + * or HTTP 503 + {@value javax.ws.rs.core.HttpHeaders#RETRY_AFTER} mechanism as described + * in the {@link EventSource} javadoc. + *
+ * The returned event source is ready, but not {@link EventSource#open() connected} to the SSE endpoint. + * It is expected that you will manually invoke its {@link #open()} method once you are ready to start + * receiving SSE events. In case you want to build an event source instance that is already connected + * to the SSE endpoint, use the event source builder {@link #open()} method instead. + *
+ * Once the event source is open, the incoming events are processed by the event source in an + * asynchronous task that runs in an internal single-threaded {@link ScheduledExecutorService + * scheduled executor service}. + *
+ * The returned event source is already {@link EventSource#open() connected} to the SSE endpoint + * and is processing any new incoming events. In case you want to build an event source instance + * that is already ready, but not automatically connected to the SSE endpoint, use the event source + * builder {@link #build()} method instead. + *
+ * The incoming events are processed by the event source in an asynchronous task that runs in an + * internal single-threaded {@link ScheduledExecutorService scheduled executor service}. + *
EventSource.target(endpoint).open()
+ * The created event source instance automatically {@link #open opens a connection} to the supplied SSE streaming + * web target and starts processing incoming {@link org.glassfish.jersey.media.sse.InboundEvent events}. + *
+ * if (open) { + * EventSource.target(endpoint).open(); + * } else { + * EventSource.target(endpoint).build(); + * }
+ * If the supplied {@code open} flag is {@code true}, the created event source instance automatically + * {@link #open opens a connection} to the supplied SSE streaming web target and starts processing incoming + * {@link org.glassfish.jersey.media.sse.InboundEvent events}. + * Otherwise, if the {@code open} flag is set to {@code false}, the created event source instance + * is not automatically connected to the web target. In this case it is expected that the user who + * created the event source will manually invoke its {@link #open()} method. + *
+ * The default {@code EventSource} implementation is empty, users can override this method to handle + * incoming {@link org.glassfish.jersey.media.sse.InboundEvent}s. + *
+ * Note that overriding this method may be necessary to make sure no {@code InboundEvent incoming events} + * are lost in case the event source is constructed using {@link #EventSource(javax.ws.rs.client.WebTarget)} + * constructor or in case a {@code true} flag is passed to the {@link #EventSource(javax.ws.rs.client.WebTarget, boolean, javax.ws.rs.core.MultivaluedMap)} + * constructor, since the connection is opened as as part of the constructor call and the event processing starts + * immediately. Therefore any {@link EventListener}s registered later after the event source has been constructed + * may miss the notifications about the one or more events that arrive immediately after the connection to the + * event source is established. + *
+ * The method blocks until the event processing task has completed execution after a shutdown + * request, or until the timeout occurs, or the current thread is interrupted, whichever happens + * first. + *
+ * In case the waiting for the event processing task has been interrupted, this method restores + * the {@link Thread#interrupted() interrupt} flag on the thread before returning {@code false}. + *
+ * The method will silently abort in case the event source is not {@link EventSource#isOpen() open}. + *
* For example: + *
* EventSource es = EventSource.target(endpoint).named("my source") * .reconnectingEvery(5, SECONDS) * .open(); *