From 26dc142e075d6f816eac2f950faaefee366487f2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 11 Dec 2018 14:26:43 -0800 Subject: [PATCH 1/6] add time threshold for backoff reset --- README.md | 2 +- .../launchdarkly/eventsource/EventSource.java | 41 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d35f887..191a81e 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,4 @@ Java EventSource implementation based on OkHttp Project Information ----------- -This libary allows Java developers to consume Server Sent Events from a remote API. The server sent events spec is defined here: [https://html.spec.whatwg.org/multipage/server-sent-events.html](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) +This library allows Java developers to consume Server Sent Events from a remote API. The server sent events spec is defined here: [https://html.spec.whatwg.org/multipage/server-sent-events.html](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) diff --git a/src/main/java/com/launchdarkly/eventsource/EventSource.java b/src/main/java/com/launchdarkly/eventsource/EventSource.java index b6b7377..b9f415d 100644 --- a/src/main/java/com/launchdarkly/eventsource/EventSource.java +++ b/src/main/java/com/launchdarkly/eventsource/EventSource.java @@ -7,8 +7,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.launchdarkly.eventsource.ConnectionErrorHandler.Action; - import javax.annotation.Nullable; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; @@ -19,7 +17,6 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; -import java.net.SocketTimeoutException; import java.net.Proxy.Type; import java.net.URI; import java.security.GeneralSecurityException; @@ -48,6 +45,7 @@ public class EventSource implements ConnectionHandler, Closeable { static final int DEFAULT_CONNECT_TIMEOUT_MS = 10000; static final int DEFAULT_WRITE_TIMEOUT_MS = 5000; static final int DEFAULT_READ_TIMEOUT_MS = 1000 * 60 * 5; + static final int DEFAULT_BACKOFF_RESET_THRESHOLD_MS = 1000 * 60; private final String name; private volatile URI uri; @@ -56,8 +54,9 @@ public class EventSource implements ConnectionHandler, Closeable { @Nullable private final RequestBody body; private final ExecutorService eventExecutor; private final ExecutorService streamExecutor; - private long reconnectTimeMs = 0; + private long reconnectTimeMs; private long maxReconnectTimeMs; + private final long backoffResetThresholdMs; private volatile String lastEventId; private final EventHandler handler; private final ConnectionErrorHandler connectionErrorHandler; @@ -77,6 +76,7 @@ public class EventSource implements ConnectionHandler, Closeable { this.body = builder.body; this.reconnectTimeMs = builder.reconnectTimeMs; this.maxReconnectTimeMs = builder.maxReconnectTimeMs; + this.backoffResetThresholdMs = builder.backoffResetThresholdMs; ThreadFactory eventsThreadFactory = createThreadFactory("okhttp-eventsource-events"); this.eventExecutor = Executors.newSingleThreadExecutor(eventsThreadFactory); ThreadFactory streamThreadFactory = createThreadFactory("okhttp-eventsource-stream"); @@ -179,16 +179,15 @@ private void connect() { try { while (!Thread.currentThread().isInterrupted() && readyState.get() != SHUTDOWN) { - boolean gotResponse = false, timedOut = false; + long connectedTime = -1; - maybeWaitWithBackoff(reconnectAttempts++); ReadyState currentState = readyState.getAndSet(CONNECTING); logger.debug("readyState change: " + currentState + " -> " + CONNECTING); - try { + try { call = client.newCall(buildRequest()); response = call.execute(); if (response.isSuccessful()) { - gotResponse = true; + connectedTime = System.currentTimeMillis(); currentState = readyState.getAndSet(OPEN); if (currentState != CONNECTING) { logger.warn("Unexpected readyState change: " + currentState + " -> " + OPEN); @@ -222,9 +221,6 @@ private void connect() { } else { errorHandlerAction = ConnectionErrorHandler.Action.SHUTDOWN; } - if (ioe instanceof SocketTimeoutException) { - timedOut = true; - } } finally { ReadyState nextState = CLOSED; if (errorHandlerAction == ConnectionErrorHandler.Action.SHUTDOWN) { @@ -255,10 +251,12 @@ private void connect() { handler.onError(e); } } - // reset the backoff if we had a successful connection that was dropped for non-timeout reasons - if (gotResponse && !timedOut) { + // Reset the backoff if we had a successful connection that stayed good for at least + // backoffResetThresholdMs milliseconds. + if (connectedTime >= 0 && (System.currentTimeMillis() - connectedTime) >= backoffResetThresholdMs) { reconnectAttempts = 0; } + maybeWaitWithBackoff(++reconnectAttempts); } } } catch (RejectedExecutionException ignored) { @@ -359,6 +357,7 @@ public static final class Builder { private String name = ""; private long reconnectTimeMs = DEFAULT_RECONNECT_TIME_MS; private long maxReconnectTimeMs = DEFAULT_MAX_RECONNECT_TIME_MS; + private long backoffResetThresholdMs = DEFAULT_BACKOFF_RESET_THRESHOLD_MS; private final URI uri; private final EventHandler handler; private ConnectionErrorHandler connectionErrorHandler = ConnectionErrorHandler.DEFAULT; @@ -442,6 +441,22 @@ public Builder maxReconnectTimeMs(long maxReconnectTimeMs) { return this; } + /** + * Sets the minimum amount of time that a connection must stay open before the EventSource resets its + * backoff delay. If a connection fails before the threshold has elapsed, the delay before reconnecting + * will be greater than the last delay; if it fails after the threshold, the delay will start over at + * the initial minimum value. This prevents long delays from occurring on connections that are only + * rarely restarted. + * + * @param backoffResetThresholdMs the minimum time in milliseconds that a connection must stay open to + * avoid resetting the delay + * @return the builder + */ + public Builder backoffResetThresholdMs(long backoffResetThresholdMs) { + this.backoffResetThresholdMs = backoffResetThresholdMs; + return this; + } + /** * Set the headers to be sent when establishing the EventSource connection. * From ca0865d990fb534f9717cfa28aaa7d3540b53067 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 11 Dec 2018 15:05:27 -0800 Subject: [PATCH 2/6] allow endpoint to be specified as either URI or HttpUrl --- .../launchdarkly/eventsource/EventSource.java | 93 +++++++++++++++++-- .../eventsource/EventSourceTest.java | 71 +++++++++++++- 2 files changed, 153 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/launchdarkly/eventsource/EventSource.java b/src/main/java/com/launchdarkly/eventsource/EventSource.java index b6b7377..f875cb5 100644 --- a/src/main/java/com/launchdarkly/eventsource/EventSource.java +++ b/src/main/java/com/launchdarkly/eventsource/EventSource.java @@ -50,7 +50,7 @@ public class EventSource implements ConnectionHandler, Closeable { static final int DEFAULT_READ_TIMEOUT_MS = 1000 * 60 * 5; private final String name; - private volatile URI uri; + private volatile HttpUrl url; private final Headers headers; private final String method; @Nullable private final RequestBody body; @@ -71,7 +71,7 @@ public class EventSource implements ConnectionHandler, Closeable { EventSource(Builder builder) { this.name = builder.name; this.logger = LoggerFactory.getLogger(EventSource.class.getCanonicalName() + "." + name); - this.uri = builder.uri; + this.url = builder.url; this.headers = addDefaultHeaders(builder.headers); this.method = builder.method; this.body = builder.body; @@ -108,7 +108,7 @@ public void start() { return; } logger.debug("readyState change: " + RAW + " -> " + CONNECTING); - logger.info("Starting EventSource client using URI: " + uri); + logger.info("Starting EventSource client using URI: " + url); streamExecutor.execute(new Runnable() { public void run() { connect(); @@ -158,10 +158,11 @@ public void close() { } } } + Request buildRequest() { Request.Builder builder = new Request.Builder() .headers(headers) - .url(uri.toASCIIString()) + .url(url) .method(method, body); if (lastEventId != null && !lastEventId.isEmpty()) { @@ -205,7 +206,7 @@ private void connect() { bufferedSource.close(); } bufferedSource = Okio.buffer(response.body().source()); - EventParser parser = new EventParser(uri, handler, EventSource.this); + EventParser parser = new EventParser(url.uri(), handler, EventSource.this); for (String line; !Thread.currentThread().isInterrupted() && (line = bufferedSource.readUtf8LineStrict()) != null; ) { parser.line(line); } @@ -347,19 +348,61 @@ public void setLastEventId(String lastEventId) { this.lastEventId = lastEventId; } + /** + * Returns the current stream endpoint as an OkHttp HttpUrl. + * + * @since 1.9.0 + */ + public HttpUrl getHttpUrl() { + return this.url; + } + + /** + * Returns the current stream endpoint as a java.net.URI. + */ public URI getUri() { - return this.uri; + return this.url.uri(); } + /** + * Changes the stream endpoint. This change will not take effect until the next time the + * EventSource attempts to make a connection. + * + * @param url the new endpoint, as an OkHttp HttpUrl + * @throws IllegalArgumentException if the parameter is null or if the scheme is not HTTP or HTTPS + * + * @since 1.9.0 + */ + public void setHttpUrl(HttpUrl url) { + if (url == null) { + throw badUrlException(); + } + this.url = url; + } + + /** + * Changes the stream endpoint. This change will not take effect until the next time the + * EventSource attempts to make a connection. + * + * @param url the new endpoint, as a java.net.URI + * @throws IllegalArgumentException if the parameter is null or if the scheme is not HTTP or HTTPS + */ public void setUri(URI uri) { - this.uri = uri; + setHttpUrl(uri == null ? null : HttpUrl.get(uri)); } + private static IllegalArgumentException badUrlException() { + return new IllegalArgumentException("URI/URL must not be null and must be HTTP or HTTPS"); + } + + /** + * Builder for {@link EventSource}. + */ public static final class Builder { private String name = ""; private long reconnectTimeMs = DEFAULT_RECONNECT_TIME_MS; private long maxReconnectTimeMs = DEFAULT_MAX_RECONNECT_TIME_MS; - private final URI uri; + private final HttpUrl url; private final EventHandler handler; private ConnectionErrorHandler connectionErrorHandler = ConnectionErrorHandler.DEFAULT; private Headers headers = Headers.of(); @@ -374,11 +417,37 @@ public static final class Builder { .writeTimeout(DEFAULT_WRITE_TIMEOUT_MS, TimeUnit.MILLISECONDS) .retryOnConnectionFailure(true); + /** + * Creates a new builder. + * + * @param handler the event handler + * @param uri the endpoint as a java.net.URI + * @throws IllegalArgumentException if either argument is null, or if the endpoint is not HTTP or HTTPS + */ public Builder(EventHandler handler, URI uri) { - this.uri = uri; - this.handler = handler; + this(handler, uri == null ? null : HttpUrl.get(uri)); } + /** + * Creates a new builder. + * + * @param handler the event handler + * @param uri the endpoint as an OkHttp HttpUrl + * @throws IllegalArgumentException if either argument is null, or if the endpoint is not HTTP or HTTPS + * + * @since 1.9.0 + */ + public Builder(EventHandler handler, HttpUrl url) { + if (handler == null) { + throw new IllegalArgumentException("handler must not be null"); + } + if (url == null) { + throw badUrlException(); + } + this.url = url; + this.handler = handler; + } + /** * Set the HTTP method used for this EventSource client to use for requests to establish the EventSource. * @@ -547,6 +616,10 @@ public Builder connectionErrorHandler(ConnectionErrorHandler handler) { return this; } + /** + * Constructs an {@link EventSource} using the builder's current properties. + * @return the new EventSource instance + */ public EventSource build() { if (proxy != null) { clientBuilder.proxy(proxy); diff --git a/src/test/java/com/launchdarkly/eventsource/EventSourceTest.java b/src/test/java/com/launchdarkly/eventsource/EventSourceTest.java index a6212da..5a8880c 100644 --- a/src/test/java/com/launchdarkly/eventsource/EventSourceTest.java +++ b/src/test/java/com/launchdarkly/eventsource/EventSourceTest.java @@ -1,5 +1,6 @@ package com.launchdarkly.eventsource; +import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -20,16 +21,84 @@ import static org.mockito.Mockito.mock; public class EventSourceTest { + private static final URI STREAM_URI = URI.create("http://www.example.com/"); + private static final HttpUrl STREAM_HTTP_URL = HttpUrl.parse("http://www.example.com/"); private EventSource eventSource; private EventSource.Builder builder; @Before public void setUp() { EventHandler eventHandler = mock(EventHandler.class); - builder = new EventSource.Builder(eventHandler, URI.create("http://www.example.com")); + builder = new EventSource.Builder(eventHandler, STREAM_URI); eventSource = builder.build(); } + @Test + public void hasExpectedUri() { + assertEquals(STREAM_URI, eventSource.getUri()); + } + + @Test + public void hasExpectedUriWhenInitializedWithHttpUrl() { + EventSource es = new EventSource.Builder(mock(EventHandler.class), STREAM_HTTP_URL).build(); + assertEquals(STREAM_URI, es.getUri()); + } + + @Test + public void hasExpectedHttpUrlWhenInitializedWithUri() { + assertEquals(STREAM_HTTP_URL, eventSource.getHttpUrl()); + } + + @Test + public void hasExpectedHttpUrlWhenInitializedWithHttpUrl() { + EventSource es = new EventSource.Builder(mock(EventHandler.class), STREAM_HTTP_URL).build(); + assertEquals(STREAM_HTTP_URL, es.getHttpUrl()); + } + + @Test(expected=IllegalArgumentException.class) + public void handlerCannotBeNull() { + new EventSource.Builder(null, STREAM_URI); + } + + @Test(expected=IllegalArgumentException.class) + public void uriCannotBeNull() { + new EventSource.Builder(mock(EventHandler.class), (URI)null); + } + + @Test(expected=IllegalArgumentException.class) + public void httpUrlCannotBeNull() { + new EventSource.Builder(mock(EventHandler.class), (HttpUrl)null); + } + + @Test + public void canSetUri() { + URI uri = URI.create("http://www.other.com/"); + eventSource.setUri(uri); + assertEquals(uri, eventSource.getUri()); + } + + @Test(expected=IllegalArgumentException.class) + public void cannotSetUriToNull() { + eventSource.setUri(null); + } + + @Test(expected=IllegalArgumentException.class) + public void cannotSetUriToInvalidScheme() { + eventSource.setUri(URI.create("gopher://example.com/")); + } + + @Test + public void canSetHttpUrl() { + HttpUrl url = HttpUrl.parse("http://www.other.com/"); + eventSource.setHttpUrl(url); + assertEquals(url, eventSource.getHttpUrl()); + } + + @Test(expected=IllegalArgumentException.class) + public void cannotSetHttpUrlToNull() { + eventSource.setHttpUrl(null); + } + @Test public void respectsDefaultMaximumBackoffTime() { eventSource.setReconnectionTimeMs(2000); From a35c7e62905461cde2f4e4f3ec98d2bc4f2351cb Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 11 Dec 2018 15:09:24 -0800 Subject: [PATCH 3/6] add @since --- src/main/java/com/launchdarkly/eventsource/EventSource.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/launchdarkly/eventsource/EventSource.java b/src/main/java/com/launchdarkly/eventsource/EventSource.java index b9f415d..694bd18 100644 --- a/src/main/java/com/launchdarkly/eventsource/EventSource.java +++ b/src/main/java/com/launchdarkly/eventsource/EventSource.java @@ -451,6 +451,8 @@ public Builder maxReconnectTimeMs(long maxReconnectTimeMs) { * @param backoffResetThresholdMs the minimum time in milliseconds that a connection must stay open to * avoid resetting the delay * @return the builder + * + * @since 1.9.0 */ public Builder backoffResetThresholdMs(long backoffResetThresholdMs) { this.backoffResetThresholdMs = backoffResetThresholdMs; From 8e4363b2c4d21373f7556dd78b93d0595041256e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 11 Dec 2018 15:29:37 -0800 Subject: [PATCH 4/6] add interface for customizing requests --- .../launchdarkly/eventsource/EventSource.java | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/launchdarkly/eventsource/EventSource.java b/src/main/java/com/launchdarkly/eventsource/EventSource.java index b6b7377..9f1b2d2 100644 --- a/src/main/java/com/launchdarkly/eventsource/EventSource.java +++ b/src/main/java/com/launchdarkly/eventsource/EventSource.java @@ -54,6 +54,7 @@ public class EventSource implements ConnectionHandler, Closeable { private final Headers headers; private final String method; @Nullable private final RequestBody body; + private final RequestTransformer requestTransformer; private final ExecutorService eventExecutor; private final ExecutorService streamExecutor; private long reconnectTimeMs = 0; @@ -75,6 +76,7 @@ public class EventSource implements ConnectionHandler, Closeable { this.headers = addDefaultHeaders(builder.headers); this.method = builder.method; this.body = builder.body; + this.requestTransformer = builder.requestTransformer; this.reconnectTimeMs = builder.reconnectTimeMs; this.maxReconnectTimeMs = builder.maxReconnectTimeMs; ThreadFactory eventsThreadFactory = createThreadFactory("okhttp-eventsource-events"); @@ -158,6 +160,7 @@ public void close() { } } } + Request buildRequest() { Request.Builder builder = new Request.Builder() .headers(headers) @@ -167,7 +170,9 @@ Request buildRequest() { if (lastEventId != null && !lastEventId.isEmpty()) { builder.addHeader("Last-Event-ID", lastEventId); } - return builder.build(); + + Request request = builder.build(); + return requestTransformer == null ? request : requestTransformer.transformRequest(request); } private void connect() { @@ -355,6 +360,36 @@ public void setUri(URI uri) { this.uri = uri; } + /** + * Interface for an object that can modify the network request that the EventSource will make. + * Use this in conjunction with {@link Builder#requestTransformer} if you need to set request + * properties other than the ones that are already supported by the builder (or if, for + * whatever reason, you need to determine the request properties dynamically rather than + * setting them to fixed values initially). For example: + * + * public class RequestTagger implements EventSource.RequestTransformer { + * public Request transformRequest(Request input) { + * return input.newBuilder().tag("hello").build(); + * } + * } + * + * EventSource es = new EventSource.Builder(handler, uri).requestTransformer(new RequestTagger()).build(); + * + * + * @since 1.9.0 + */ + public static interface RequestTransformer { + /** + * Returns a request that is either the same as the input request or based on it. When + * this method is called, EventSource has already set all of its standard properties on + * the request. + * + * @param input the original request + * @return the request that will be used + */ + public Request transformRequest(Request input); + } + public static final class Builder { private String name = ""; private long reconnectTimeMs = DEFAULT_RECONNECT_TIME_MS; @@ -366,6 +401,7 @@ public static final class Builder { private Proxy proxy; private Authenticator proxyAuthenticator = null; private String method = "GET"; + private RequestTransformer requestTransformer = null; @Nullable private RequestBody body = null; private OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(1, 1, TimeUnit.SECONDS)) @@ -404,6 +440,19 @@ public Builder body(@Nullable RequestBody body) { return this; } + /** + * Specifies an object that will be used to customize outgoing requests. See {@link RequestTransformer} for details. + * + * @param requestTransformer the transformer object + * @return the builder + * + * @since 1.9.0 + */ + public Builder requestTransformer(@Nullable RequestTransformer requestTransformer) { + this.requestTransformer = requestTransformer; + return this; + } + /** * Set the name for this EventSource client to be used when naming the logger and threadpools. This is mainly useful when * multiple EventSource clients exist within the same process. From 52d1732257c0ae3da0b57ecc80603e0032ab2646 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 12 Dec 2018 17:09:57 -0800 Subject: [PATCH 5/6] javadoc fixes --- gradle.properties | 2 +- .../eventsource/ConnectionErrorHandler.java | 8 ++ .../eventsource/EventHandler.java | 26 ++++ .../launchdarkly/eventsource/EventParser.java | 5 + .../launchdarkly/eventsource/EventSource.java | 136 +++++++++++++----- .../eventsource/MessageEvent.java | 25 ++++ .../launchdarkly/eventsource/ReadyState.java | 18 +++ .../UnsuccessfulResponseException.java | 12 ++ 8 files changed, 199 insertions(+), 33 deletions(-) diff --git a/gradle.properties b/gradle.properties index d4e390c..96aa496 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=1.8.0 +version=1.9.0-SNAPSHOT ossrhUsername= ossrhPassword= diff --git a/src/main/java/com/launchdarkly/eventsource/ConnectionErrorHandler.java b/src/main/java/com/launchdarkly/eventsource/ConnectionErrorHandler.java index 39d0d3c..f663ce1 100644 --- a/src/main/java/com/launchdarkly/eventsource/ConnectionErrorHandler.java +++ b/src/main/java/com/launchdarkly/eventsource/ConnectionErrorHandler.java @@ -1,5 +1,10 @@ package com.launchdarkly.eventsource; +/** + * Interface for an object that will be notified when EventSource encounters a connection failure. + * This is different from {@link EventHandler#onError(Throwable)} in that it will not be called for + * other kinds of errors; also, it has the ability to tell EventSource to stop reconnecting. + */ public interface ConnectionErrorHandler { /** * Return values of {@link ConnectionErrorHandler#onConnectionError(Throwable)} indicating what @@ -28,6 +33,9 @@ public static enum Action { */ Action onConnectionError(Throwable t); + /** + * Default handler that does nothing. + */ public static final ConnectionErrorHandler DEFAULT = new ConnectionErrorHandler() { @Override public Action onConnectionError(Throwable t) { diff --git a/src/main/java/com/launchdarkly/eventsource/EventHandler.java b/src/main/java/com/launchdarkly/eventsource/EventHandler.java index f1dfd86..9d82192 100644 --- a/src/main/java/com/launchdarkly/eventsource/EventHandler.java +++ b/src/main/java/com/launchdarkly/eventsource/EventHandler.java @@ -1,10 +1,36 @@ package com.launchdarkly.eventsource; +/** + * Interface for an object that will receive SSE events. + */ public interface EventHandler { + /** + * EventSource calls this method when the stream connection has been opened. + * @throws Exception throwing an exception here will cause it to be logged and also sent to {@link #onError(Throwable)} + */ void onOpen() throws Exception; + + /** + * EventSource calls this method when the stream connection has been closed. + * @throws Exception throwing an exception here will cause it to be logged and also sent to {@link #onError(Throwable)} + */ void onClosed() throws Exception; + + /** + * EventSource calls this method when it has received a new event from the stream. + * @param event the event name, from the {@code event:} line in the stream + * @param messageEvent a {@link MessageEvent} object containing all the other event properties + * @throws Exception throwing an exception here will cause it to be logged and also sent to {@link #onError(Throwable)} + */ void onMessage(String event, MessageEvent messageEvent) throws Exception; + + /** + * EventSource calls this method when it has received a comment line from the stream (any line starting with a colon). + * @param comment the comment line + * @throws Exception throwing an exception here will cause it to be logged and also sent to {@link #onError(Throwable)} + */ void onComment(String comment) throws Exception; + /** * This method will be called for all exceptions that occur on the socket connection (including * an {@link UnsuccessfulResponseException} if the server returns an unexpected HTTP status), diff --git a/src/main/java/com/launchdarkly/eventsource/EventParser.java b/src/main/java/com/launchdarkly/eventsource/EventParser.java index 9266621..fb0f92d 100644 --- a/src/main/java/com/launchdarkly/eventsource/EventParser.java +++ b/src/main/java/com/launchdarkly/eventsource/EventParser.java @@ -34,6 +34,11 @@ public class EventParser { this.connectionHandler = connectionHandler; } + /** + * Accepts a single line of input and updates the parser state. If this completes a valid event, + * the event is sent to the {@link EventHandler}. + * @param line an input line + */ public void line(String line) { logger.debug("Parsing line: " + line); int colonIndex; diff --git a/src/main/java/com/launchdarkly/eventsource/EventSource.java b/src/main/java/com/launchdarkly/eventsource/EventSource.java index 882b45c..3916c17 100644 --- a/src/main/java/com/launchdarkly/eventsource/EventSource.java +++ b/src/main/java/com/launchdarkly/eventsource/EventSource.java @@ -40,12 +40,30 @@ public class EventSource implements ConnectionHandler, Closeable { private final Logger logger; - private static final long DEFAULT_RECONNECT_TIME_MS = 1000; - static final long DEFAULT_MAX_RECONNECT_TIME_MS = 30000; - static final int DEFAULT_CONNECT_TIMEOUT_MS = 10000; - static final int DEFAULT_WRITE_TIMEOUT_MS = 5000; - static final int DEFAULT_READ_TIMEOUT_MS = 1000 * 60 * 5; - static final int DEFAULT_BACKOFF_RESET_THRESHOLD_MS = 1000 * 60; + /** + * The default value for {@link Builder#reconnectTimeMs(long)}: 1000 (1 second). + */ + public static final long DEFAULT_RECONNECT_TIME_MS = 1000; + /** + * The default value for {@link Builder#maxReconnectTimeMs(long)}: 30000 (30 seconds). + */ + public static final long DEFAULT_MAX_RECONNECT_TIME_MS = 30000; + /** + * The default value for {@link Builder#connectTimeoutMs(int)}: 10000 (10 seconds). + */ + public static final int DEFAULT_CONNECT_TIMEOUT_MS = 10000; + /** + * The default value for {@link Builder#writeTimeoutMs(int)}: 5000 (5 seconds). + */ + public static final int DEFAULT_WRITE_TIMEOUT_MS = 5000; + /** + * The default value for {@link Builder#readTimeoutMs(int)}: 300000 (5 minutes). + */ + public static final int DEFAULT_READ_TIMEOUT_MS = 1000 * 60 * 5; + /** + * The default value for {@link Builder#backoffResetThresholdMs(long)}: 60000 (60 seconds). + */ + public static final int DEFAULT_BACKOFF_RESET_THRESHOLD_MS = 1000 * 60; private final String name; private volatile HttpUrl url; @@ -104,6 +122,10 @@ public Thread newThread(Runnable runnable) { }; } + /** + * Attempts to connect to the remote event source if not already connected. This method returns + * immediately; the connection happens on a worker thread. + */ public void start() { if (!readyState.compareAndSet(RAW, CONNECTING)) { logger.info("Start method called on this already-started EventSource object. Doing nothing"); @@ -118,6 +140,10 @@ public void run() { }); } + /** + * Returns an enum indicating the current status of the connection. + * @return a {@link ReadyState} value + */ public ReadyState getState() { return readyState.get(); } @@ -334,26 +360,56 @@ private static Headers addDefaultHeaders(Headers custom) { return builder.build(); } + /** + * Sets the minimum delay between connection attempts. The actual delay may be slightly less or + * greater, since there is a random jitter. When there is a connection failure, the delay will + * start at this value and will increase exponentially up to the {@link #setMaxReconnectTimeMs(long)} + * value with each subsequent failure, unless it is reset as described in + * {@link Builder#backoffResetThresholdMs(long)}. + * @param reconnectionTimeMs the minimum delay in milliseconds + * @see #setMaxReconnectTimeMs(long) + * @see Builder#reconnectTimeMs(long) + * @see #DEFAULT_RECONNECT_TIME_MS + */ public void setReconnectionTimeMs(long reconnectionTimeMs) { this.reconnectTimeMs = reconnectionTimeMs; } + /** + * Sets the maximum delay between connection attempts. See {@link #setReconnectionTimeMs(long)}. + * The default value is 30000 (30 seconds). + * @param maxReconnectTimeMs the maximum delay in milliseconds + * @see #setReconnectionTimeMs(long) + * @see Builder#maxReconnectTimeMs(long) + * @see #DEFAULT_MAX_RECONNECT_TIME_MS + */ public void setMaxReconnectTimeMs(long maxReconnectTimeMs) { this.maxReconnectTimeMs = maxReconnectTimeMs; } + /** + * Returns the current maximum reconnect delay as set by {@link #setReconnectionTimeMs(long)}. + * @return the maximum delay in milliseconds + */ public long getMaxReconnectTimeMs() { return this.maxReconnectTimeMs; } + /** + * Sets the ID value of the last event received. This will be sent to the remote server on the + * next connection attempt. + * @param lastEventId the last event identifier + */ public void setLastEventId(String lastEventId) { this.lastEventId = lastEventId; } /** * Returns the current stream endpoint as an OkHttp HttpUrl. - * + * @return the endpoint URL * @since 1.9.0 + * @see #getUri() + * @see #setHttpUrl(HttpUrl) */ public HttpUrl getHttpUrl() { return this.url; @@ -361,6 +417,9 @@ public HttpUrl getHttpUrl() { /** * Returns the current stream endpoint as a java.net.URI. + * @return the endpoint URI + * @see #getHttpUrl() + * @see #setUri(URI) */ public URI getUri() { return this.url.uri(); @@ -372,6 +431,8 @@ public URI getUri() { * * @param url the new endpoint, as an OkHttp HttpUrl * @throws IllegalArgumentException if the parameter is null or if the scheme is not HTTP or HTTPS + * @see #getHttpUrl() + * @see #setUri(URI) * * @since 1.9.0 */ @@ -386,8 +447,10 @@ public void setHttpUrl(HttpUrl url) { * Changes the stream endpoint. This change will not take effect until the next time the * EventSource attempts to make a connection. * - * @param url the new endpoint, as a java.net.URI + * @param uri the new endpoint, as a java.net.URI * @throws IllegalArgumentException if the parameter is null or if the scheme is not HTTP or HTTPS + * @see #getUri() + * @see #setHttpUrl(HttpUrl) */ public void setUri(URI uri) { setHttpUrl(uri == null ? null : HttpUrl.get(uri)); @@ -399,7 +462,7 @@ private static IllegalArgumentException badUrlException() { /** * Interface for an object that can modify the network request that the EventSource will make. - * Use this in conjunction with {@link Builder#requestTransformer} if you need to set request + * Use this in conjunction with {@link Builder#requestTransformer(RequestTransformer)} if you need to set request * properties other than the ones that are already supported by the builder (or if, for * whatever reason, you need to determine the request properties dynamically rather than * setting them to fixed values initially). For example: @@ -466,7 +529,7 @@ public Builder(EventHandler handler, URI uri) { * Creates a new builder. * * @param handler the event handler - * @param uri the endpoint as an OkHttp HttpUrl + * @param url the endpoint as an OkHttp HttpUrl * @throws IllegalArgumentException if either argument is null, or if the endpoint is not HTTP or HTTPS * * @since 1.9.0 @@ -535,11 +598,15 @@ public Builder name(String name) { } /** - * Set the reconnect base time for the EventSource connection in milliseconds. Reconnect attempts are computed - * from this base value with an exponential backoff and jitter. - * - * @param reconnectTimeMs the reconnect base time in milliseconds + * Sets the minimum delay between connection attempts. The actual delay may be slightly less or + * greater, since there is a random jitter. When there is a connection failure, the delay will + * start at this value and will increase exponentially up to the {@link #setMaxReconnectTimeMs(long)} + * value with each subsequent failure, unless it is reset as described in + * {@link Builder#backoffResetThresholdMs(long)}. + * @param reconnectTimeMs the minimum delay in milliseconds * @return the builder + * @see EventSource#DEFAULT_RECONNECT_TIME_MS + * @see EventSource#setReconnectionTimeMs(long) */ public Builder reconnectTimeMs(long reconnectTimeMs) { this.reconnectTimeMs = reconnectTimeMs; @@ -547,11 +614,12 @@ public Builder reconnectTimeMs(long reconnectTimeMs) { } /** - * Set the max reconnect time for the EventSource connection in milliseconds. The exponential backoff computed - * for reconnect attempts will not be larger than this value. Defaults to 30000 ms (30 seconds). - * - * @param maxReconnectTimeMs the maximum reconnect base time in milliseconds + * Sets the maximum delay between connection attempts. See {@link #setReconnectionTimeMs(long)}. + * The default value is 30000 (30 seconds). + * @param maxReconnectTimeMs the maximum delay in milliseconds * @return the builder + * @see EventSource#DEFAULT_MAX_RECONNECT_TIME_MS + * @see EventSource#setMaxReconnectTimeMs(long) */ public Builder maxReconnectTimeMs(long maxReconnectTimeMs) { this.maxReconnectTimeMs = maxReconnectTimeMs; @@ -568,6 +636,7 @@ public Builder maxReconnectTimeMs(long maxReconnectTimeMs) { * @param backoffResetThresholdMs the minimum time in milliseconds that a connection must stay open to * avoid resetting the delay * @return the builder + * @see EventSource#DEFAULT_BACKOFF_RESET_THRESHOLD_MS * * @since 1.9.0 */ @@ -626,8 +695,8 @@ public Builder proxy(Proxy proxy) { /** * Sets the Proxy Authentication mechanism if needed. Defaults to no auth. * - * @param proxyAuthenticator - * @return + * @param proxyAuthenticator the authentication mechanism + * @return the builder */ public Builder proxyAuthenticator(Authenticator proxyAuthenticator) { this.proxyAuthenticator = proxyAuthenticator; @@ -635,10 +704,11 @@ public Builder proxyAuthenticator(Authenticator proxyAuthenticator) { } /** - * Sets the connect timeout in milliseconds if needed. Defaults to {@value #DEFAULT_CONNECT_TIMEOUT_MS} + * Sets the connection timeout. * - * @param connectTimeoutMs - * @return + * @param connectTimeoutMs the connection timeout in milliseconds + * @return the builder + * @see EventSource#DEFAULT_CONNECT_TIMEOUT_MS */ public Builder connectTimeoutMs(int connectTimeoutMs) { this.clientBuilder.connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS); @@ -646,10 +716,11 @@ public Builder connectTimeoutMs(int connectTimeoutMs) { } /** - * Sets the write timeout in milliseconds if needed. Defaults to {@value #DEFAULT_WRITE_TIMEOUT_MS} + * Sets the write timeout in milliseconds. * - * @param writeTimeoutMs - * @return + * @param writeTimeoutMs the write timeout in milliseconds + * @return the builder + * @see EventSource#DEFAULT_WRITE_TIMEOUT_MS */ public Builder writeTimeoutMs(int writeTimeoutMs) { this.clientBuilder.writeTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS); @@ -657,11 +728,12 @@ public Builder writeTimeoutMs(int writeTimeoutMs) { } /** - * Sets the read timeout in milliseconds if needed. If a read timeout happens, the {@code EventSource} - * will restart the connection. Defaults to {@value #DEFAULT_READ_TIMEOUT_MS} + * Sets the read timeout in milliseconds. If a read timeout happens, the {@code EventSource} + * will restart the connection. * - * @param readTimeoutMs - * @return + * @param readTimeoutMs the read timeout in milliseconds + * @return the builder + * @see EventSource#DEFAULT_WRITE_TIMEOUT_MS */ public Builder readTimeoutMs(int readTimeoutMs) { this.clientBuilder.readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS); @@ -671,8 +743,8 @@ public Builder readTimeoutMs(int readTimeoutMs) { /** * Sets the {@link ConnectionErrorHandler} that should process connection errors. * - * @param handler - * @return + * @param handler the error handler + * @return the builder */ public Builder connectionErrorHandler(ConnectionErrorHandler handler) { if (handler != null) { diff --git a/src/main/java/com/launchdarkly/eventsource/MessageEvent.java b/src/main/java/com/launchdarkly/eventsource/MessageEvent.java index e1275f5..3c4370e 100644 --- a/src/main/java/com/launchdarkly/eventsource/MessageEvent.java +++ b/src/main/java/com/launchdarkly/eventsource/MessageEvent.java @@ -2,29 +2,54 @@ import java.net.URI; +/** + * Event information that is passed to {@link EventHandler#onMessage(String, MessageEvent)}. + */ public class MessageEvent { private final String data; private final String lastEventId; private final URI origin; + /** + * Constructs a new instance. + * @param data the event data, if any + * @param lastEventId the event ID, if any + * @param origin the stream endpoint + */ public MessageEvent(String data, String lastEventId, URI origin) { this.data = data; this.lastEventId = lastEventId; this.origin = origin; } + /** + * Constructs a new instance. + * @param data the event data, if any + */ public MessageEvent(String data) { this(data, null, null); } + /** + * Returns the event data, if any. + * @return the data string or null + */ public String getData() { return data; } + /** + * Returns the event ID, if any. + * @return the event ID or null + */ public String getLastEventId() { return lastEventId; } + /** + * Returns the endpoint of the stream that generated the event. + * @return the stream URI + */ public URI getOrigin() { return origin; } diff --git a/src/main/java/com/launchdarkly/eventsource/ReadyState.java b/src/main/java/com/launchdarkly/eventsource/ReadyState.java index 93adff4..9bca5d0 100644 --- a/src/main/java/com/launchdarkly/eventsource/ReadyState.java +++ b/src/main/java/com/launchdarkly/eventsource/ReadyState.java @@ -1,9 +1,27 @@ package com.launchdarkly.eventsource; +/** + * Enum values that can be returned by {@link EventSource#getState()}. + */ public enum ReadyState { + /** + * The EventSource's {@link EventSource#start()} method has not yet been called. + */ RAW, + /** + * The EventSource is attempting to make a connection. + */ CONNECTING, + /** + * The connection is active and the EventSource is listening for events. + */ OPEN, + /** + * The connection has been closed or has failed, and the EventSource will attempt to reconnect. + */ CLOSED, + /** + * The connection has been permanently closed and will not reconnect. + */ SHUTDOWN } diff --git a/src/main/java/com/launchdarkly/eventsource/UnsuccessfulResponseException.java b/src/main/java/com/launchdarkly/eventsource/UnsuccessfulResponseException.java index 6bdd232..40b5a24 100644 --- a/src/main/java/com/launchdarkly/eventsource/UnsuccessfulResponseException.java +++ b/src/main/java/com/launchdarkly/eventsource/UnsuccessfulResponseException.java @@ -1,14 +1,26 @@ package com.launchdarkly.eventsource; +/** + * Exception class that means the remote server returned an HTTP error. + */ +@SuppressWarnings("serial") public class UnsuccessfulResponseException extends Exception { private final int code; + /** + * Constructs an exception instance. + * @param code the HTTP status + */ public UnsuccessfulResponseException(int code) { super("Unsuccessful response code received from stream: " + code); this.code = code; } + /** + * Returns the HTTP status code. + * @return the HTTP status + */ public int getCode() { return code; } From e82ee5f57d7c446bc3ababf48fcbe9316cabea74 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 12 Dec 2018 17:10:52 -0800 Subject: [PATCH 6/6] add changelog for past releases --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2dd7320 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Change log + +All notable changes to the LaunchDarkly EventSource implementation for Java will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). + +## [1.8.0] - 2018-04-04 +### Added: +- Added `maxReconnectTimeMs(long)` method to `EventSource.Builder` to override the default maximum reconnect time of 30 seconds + +## [1.7.1] - 2018-01-30 +### Changed: +- Fixed EventSource logger name to match convention +- Ensure background threads are daemon threads +- Don't log IOExceptions when the stream is already being shut down + +## [1.7.0] - 2018-01-07 +### Added: +- Added the ability to connect to an SSE resource using any HTTP method (defaults to GET), and specify a request body. + +## [1.6.0] - 2017-12-20 +### Added: +- Add new handler interface for stopping connection retries after an error + +### Fixed: +- Ensure that connections are closed completely ([#24](https://github.com/launchdarkly/okhttp-eventsource/pull/24)) +- Ensure that we reconnect with backoff in case of a read timeout