com.launchdarkly.eventsource.EventSource Maven / Gradle / Ivy
Show all versions of okhttp-eventsource Show documentation
package com.launchdarkly.eventsource;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.logging.LogValues;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static com.launchdarkly.eventsource.Helpers.millisFromTimeUnit;
import static com.launchdarkly.eventsource.ReadyState.RAW;
import static com.launchdarkly.eventsource.ReadyState.SHUTDOWN;
import okhttp3.HttpUrl;
/**
* The SSE client.
*
* By default, EventSource makes HTTP requests using OkHttp, but it can be configured
* to read from any input stream. See {@link ConnectStrategy} and {@link HttpConnectStrategy}.
*
* Instances are always configured and constructed with {@link Builder}. The client is
* created in an inactive state.
*
* The client uses a pull model where the caller starts the EventSource and then requests
* data from it synchronously on a single thread. The initial connection attempt is made
* when you call {@link #start()}, or when you first attempt to read an event.
*
* To read events from the stream, you can either request them one at a time by calling
* {@link #readMessage()} or {@link #readAnyEvent()}, or consume them in a loop by calling
* {@link #messages()} or {@link #anyEvents()}. The "message" methods assume you are only
* interested in {@link MessageEvent} data, whereas the "anyEvent" methods also provide
* other kinds of stream information. These are blocking methods with no timeout; if you
* need a timeout mechanism, consider reading from the stream on a worker thread and
* using a queue such as {@link BlockingQueue} to consume the messages elsewhere.
*
* If, instead of managing your own thread to read from the stream, you would like to have
* events pushed to you from a worker thread that the library maintains, use
* {@link com.launchdarkly.eventsource.background.BackgroundEventSource}.
*
* Note that although {@code EventSource} is named after the JavaScript API that is described
* in the SSE specification, its behavior is not necessarily identical to standard web browser
* implementations of EventSource: by default, it will automatically retry (with a backoff
* delay) for some error conditions where a browser will not retry, and it also supports
* request configuration options (such as request headers and method) that the browser
* EventSource does not support. However, its interpretation of the stream data is fully
* conformant with the SSE specification, unless you use the opt-in mode
* {@link Builder#streamEventData(boolean)} which allows for greater efficiency in some use
* cases but has some behavioral constraints.
*/
public class EventSource implements Closeable {
private final LDLogger logger;
/**
* The default value for {@link Builder#retryDelay(long, TimeUnit)}: 1 second.
*/
public static final long DEFAULT_RETRY_DELAY_MILLIS = 1000;
/**
* The default value for {@link Builder#retryDelayResetThreshold(long, TimeUnit)}: 60 seconds.
*/
public static final long DEFAULT_RETRY_DELAY_RESET_THRESHOLD_MILLIS = 60000;
/**
* The default value for {@link Builder#readBufferSize(int)}.
*/
public static final int DEFAULT_READ_BUFFER_SIZE = 1000;
// Note that some fields have package-private visibility for tests.
private final Object sleepNotifier = new Object();
// The following final fields are set from the configuration builder.
private final ConnectStrategy.Client client;
final int readBufferSize;
final ErrorStrategy baseErrorStrategy;
final RetryDelayStrategy baseRetryDelayStrategy;
final long retryDelayResetThresholdMillis;
final boolean streamEventData;
final Set expectFields;
// The following mutable fields are not volatile because they should only be
// accessed from the thread that is reading from EventSource.
private EventParser eventParser;
ErrorStrategy currentErrorStrategy;
RetryDelayStrategy currentRetryDelayStrategy;
private long connectedTime;
private long disconnectedTime;
private StreamEvent nextEvent;
// These fields are set by the thread that is reading the stream, but can
// be modified from other threads if they call stop() or interrupt(). We
// use AtomicReference because we need atomicity in updates.
private final AtomicReference connectionCloser = new AtomicReference<>();
private final AtomicReference readingThread = new AtomicReference<>();
private final AtomicReference readyState;
// These fields are written by other threads if they call stop() or interrupt(),
// and are read by the thread that is reading the stream.
private volatile boolean deliberatelyClosedConnection;
private volatile boolean calledStop;
// These fields are written by the thread that is reading the stream, and can
// be read by other threads to inspect the state of the stream.
volatile long baseRetryDelayMillis; // set at config time but may be changed by a "retry:" value
private volatile String lastEventId;
private volatile URI origin;
private volatile long nextReconnectDelayMillis;
EventSource(Builder builder) {
this.logger = builder.logger == null ? LDLogger.none() : builder.logger;
this.client = builder.connectStrategy.createClient(logger);
this.origin = client.getOrigin();
this.lastEventId = builder.lastEventId;
this.baseErrorStrategy = this.currentErrorStrategy =
builder.errorStrategy == null ? ErrorStrategy.alwaysThrow() : builder.errorStrategy;
this.baseRetryDelayStrategy = this.currentRetryDelayStrategy =
(builder.retryDelayStrategy == null ? RetryDelayStrategy.defaultStrategy() :
builder.retryDelayStrategy);
this.baseRetryDelayMillis = builder.retryDelayMillis;
this.retryDelayResetThresholdMillis = builder.retryDelayResetThresholdMillis;
this.streamEventData = builder.streamEventData;
this.expectFields = builder.expectFields;
this.readBufferSize = builder.readBufferSize;
this.readyState = new AtomicReference<>(RAW);
}
/**
* Returns the stream URI.
*
* @return the stream URI
* @since 4.0.0
*/
public URI getOrigin() {
return origin;
}
/**
* Returns the logger that this EventSource is using.
*
* @return the logger
* @see Builder#logger(LDLogger)
* @since 4.0.0
*/
public LDLogger getLogger() {
return logger;
}
/**
* Returns an enum indicating the current status of the connection.
*
* @return a {@link ReadyState} value
*/
public ReadyState getState() {
return readyState.get();
}
/**
* Returns the ID value, if any, of the last known event.
*
* This can be set initially with {@link Builder#lastEventId(String)}, and is updated whenever an event
* is received that has an ID. Whether event IDs are supported depends on the server; it may ignore this
* value.
*
* @return the last known event ID, or null
* @see Builder#lastEventId(String)
* @since 2.0.0
*/
public String getLastEventId() {
return lastEventId;
}
/**
* Returns the current base retry delay.
*
* This is initially set by {@link Builder#retryDelay(long, TimeUnit)}, or
* {@link #DEFAULT_RETRY_DELAY_MILLIS} if not specified. It can be overriden by the
* stream provider if the stream contains a "retry:" line.
*
* The actual retry delay for any given reconnection is computed by applying the
* configured {@link RetryDelayStrategy} to this value.
*
* @return the base retry delay in milliseconds
* @see #getNextRetryDelayMillis()
* @since 4.0.0
*/
public long getBaseRetryDelayMillis() {
return baseRetryDelayMillis;
}
/**
* Returns the retry delay that will be used for the next reconnection, if the
* stream has failed.
*
* If you have just received a {@link StreamException} or {@link FaultEvent}, this
* value tells you how long EventSource will sleep before reconnecting, if you tell
* it to reconnect by calling {@link #start()} or by trying to read another event.
* The value is computed by applying the configured {@link RetryDelayStrategy} to
* the current value of {@link #getBaseRetryDelayMillis()}.
*
* At any other time, the value is undefined.
*
* @return the next retry delay in milliseconds
* @see #getBaseRetryDelayMillis()
* @since 4.0.0
*/
public long getNextRetryDelayMillis() {
return nextReconnectDelayMillis;
}
/**
* Attempts to start the stream if it is not already active.
*
* If there is not an active stream connection, this method attempts to start one using
* the previously configured parameters. If successful, it returns and you can proceed
* to read events. You should only read events on the same thread where you called
* {@link #start()}.
*
* If the connection fails, the behavior depends on the configured {@link ErrorStrategy}.
* The default strategy is to throw a {@link StreamException}, but you can configure it
* to continue instead, in which case {@link #start()} will keep retrying until the
* ErrorStrategy says to give up.
*
* If the stream was previously active and then failed, {@link #start()} will sleep for
* some amount of time-- the retry delay-- before trying to make the connection. The
* retry delay is determined by several factors: see {@link Builder#retryDelay(long, TimeUnit)},
* {@link Builder#retryDelayStrategy(RetryDelayStrategy)}, and
* {@link Builder#retryDelayResetThreshold(long, TimeUnit)}.
* @throws StreamException
*
* You do not necessarily need to call this method; it is implicitly called if you try
* to read an event when the stream is not active. Call it only if you specifically want
* to confirm that the stream is active before you try to read an event.
*
* If the stream is already active, calling this method has no effect.
*
* @throws StreamException if the connection attempt failed
*/
public void start() throws StreamException {
tryStart(false);
}
private FaultEvent tryStart(boolean canReturnFaultEvent) throws StreamException {
if (eventParser != null) {
return null;
}
readingThread.set(Thread.currentThread());
while (true) {
StreamException exception = null;
if (nextReconnectDelayMillis > 0) {
long delayNow = disconnectedTime == 0 ? nextReconnectDelayMillis :
(nextReconnectDelayMillis - (System.currentTimeMillis() - disconnectedTime));
if (delayNow > 0) {
logger.info("Waiting {} milliseconds before reconnecting", delayNow);
try {
synchronized (sleepNotifier) {
if (!deliberatelyClosedConnection) {
sleepNotifier.wait(delayNow);
}
// If interrupt(), stop(), or close() is called while we're waiting, we will
// trigger an early exit from this wait by calling sleepNotifier.notify().
}
} catch (InterruptedException e) {
// Thread.interrupt() should also have the effect of making us stop waiting
logger.debug("EventSource thread was interrupted during start()");
deliberatelyClosedConnection = true;
Thread.interrupted(); // clear interrupted state
}
// Check if deliberatelyClosedConnection might have been set during that wait
if (deliberatelyClosedConnection) {
exception = new StreamClosedByCallerException();
}
}
}
ConnectStrategy.Client.Result clientResult = null;
if (exception == null) {
readyState.set(ReadyState.CONNECTING);
connectedTime = 0;
deliberatelyClosedConnection = calledStop = false;
try {
clientResult = client.connect(lastEventId);
} catch (StreamException e) {
exception = e;
}
}
if (exception != null) {
disconnectedTime = System.currentTimeMillis();
computeReconnectDelay();
if (applyErrorStrategy(exception) == ErrorStrategy.Action.CONTINUE) {
// The ErrorStrategy told us to CONTINUE rather than throwing an exception.
if (canReturnFaultEvent) {
return new FaultEvent(exception);
}
// If canReturnFaultEvent is false, it means the caller explicitly called start(),
// in which case there's no way to return a FaultEvent so we just keep retrying
// transparently.
continue;
}
// The ErrorStrategy told us to THROW rather than CONTINUE.
throw exception;
}
connectionCloser.set(clientResult.getCloser());
origin = clientResult.getOrigin() == null ? client.getOrigin() : clientResult.getOrigin();
connectedTime = System.currentTimeMillis();
logger.debug("Connected to SSE stream");
eventParser = new EventParser(
clientResult.getInputStream(),
clientResult.getOrigin(),
readBufferSize,
streamEventData,
expectFields,
logger
);
readyState.set(ReadyState.OPEN);
currentErrorStrategy = baseErrorStrategy;
return null;
}
}
/**
* Attempts to receive a message from the stream.
*
* If the stream is not already active, this calls {@link #start()} to establish
* a connection.
*
* As long as the stream is active, the method blocks until a message is available.
* If the stream fails, the default behavior is to throw a {@link StreamException},
* but you can configure an {@link ErrorStrategy} to allow the client to retry
* transparently instead.
*
* This method must be called from the same thread that first started using the
* stream (that is, the thread that called {@link #start()} or read the first event).
*
* @return an SSE message
* @throws StreamException if there is an error and retry is not enabled
* @see #readAnyEvent()
* @see #messages()
* @since 4.0.0
*/
public MessageEvent readMessage() throws StreamException {
while (true) {
StreamEvent event = readAnyEvent();
if (event instanceof MessageEvent) {
return (MessageEvent)event;
}
}
}
/**
* Attempts to receive an event of any kind from the stream.
*
* This is similar to {@link #readMessage()}, except that instead of specifically
* requesting a {@link MessageEvent} it also applies to the other subclasses of
* {@link StreamEvent}: {@link StartedEvent}, {@link FaultEvent}, and {@link
* CommentEvent}. Use this method if you want to be informed of any of those
* occurrences.
*
* The error behavior is the same as {@link #readMessage()}, except that if the
* {@link ErrorStrategy} is configured to let the client continue with an
* automatic retry, you will receive a {@link FaultEvent} describing the error
* first, and then a {@link StartedEvent} once the stream is reconnected.
*
* This method must be called from the same thread that first started using the
* stream (that is, the thread that called {@link #start()} or read the first event).
*
* @return an event
* @throws StreamException if there is an error and retry is not enabled
* @see #readMessage()
* @see #anyEvents()
* @since 4.0.0
*/
public StreamEvent readAnyEvent() throws StreamException {
return requireEvent();
}
/**
* Returns an iterable sequence of SSE messages.
*
* This is similar to calling {@link #readMessage()} in a loop. If the stream
* has not already been started, it also starts the stream.
*
* The error behavior is different from {@link #readMessage()}: if an error occurs
* and the {@link ErrorStrategy} does not allow the client to continue, it simply
* stops iterating, rather than throwing an exception. If you need to be able to
* specifically detect errors, use {@link #readMessage()}.
*
* This method must be called from the same thread that first started using the
* stream (that is, the thread that called {@link #start()} or read the first event).
*
* @return a sequence of SSE messages
* @see #readAnyEvent()
* @see #messages()
* @since 4.0.0
*/
public Iterable messages() {
return new Iterable() {
@Override
public Iterator iterator() {
return new IteratorImpl<>(MessageEvent.class);
}
};
}
/**
* Returns an iterable sequence of events.
*
* This is similar to calling {@link #readAnyEvent()} in a loop. If the stream
* has not already been started, it also starts the stream.
*
* The error behavior is different from {@link #readAnyEvent()}: if an error occurs
* and the {@link ErrorStrategy} does not allow the client to continue, it simply
* stops iterating, rather than throwing an exception. If you need to be able to
* specifically detect errors, use {@link #readAnyEvent()} (or, use the
* {@link ErrorStrategy} mechanism to cause errors to be reported as
* {@link FaultEvent}s).
*
* This method must be called from the same thread that first started using the
* stream (that is, the thread that called {@link #start()} or read the first event).
*
* @return a sequence of events
* @see #readAnyEvent()
* @see #messages()
* @since 4.0.0
*/
public Iterable anyEvents() {
return new Iterable() {
@Override
public Iterator iterator() {
return new IteratorImpl<>(StreamEvent.class);
}
};
}
/**
* Stops the stream connection if it is currently active.
*
* Unlike the reading methods, you are allowed to call this method from any
* thread. If you are reading events on a different thread, and automatic
* retries are not enabled by an {@link ErrorStrategy}, the other thread will
* receive a {@link StreamClosedByCallerException}.
*
* The difference between this method and {@link #stop()} is only relevant if
* automatic retries are enabled. In this case, if you are using the
* {@link #messages()} or {@link #anyEvents()} iterator to read events, calling
* {@link #interrupt()} will cause the stream to be closed and then immediately
* reconnected, whereas {@link #stop()} will close it and then the iterator
* will end. In either case, if you explicitly try to read another event it
* will start the stream again.
*
* If the stream is not currently active, calling this method has no effect.
*
* Note for Android developers: since it is generally undesirable to perform
* any network activity from the main thread, be aware that {@link #interrupt()},
* {@link #stop()}, and {@link #close()} all cause an immediate close of the
* connection (if any), which happens on the same thread that called the method.
*
* @since 4.0.0
* @see #stop()
* @see #start()
*/
public void interrupt() {
closeCurrentStream(true, false);
}
/**
* Stops the stream connection if it is currently active.
*
* Unlike the reading methods, you are allowed to call this method from any
* thread. If you are reading events on a different thread, and automatic
* retries are not enabled by an {@link ErrorStrategy}, the other thread will
* receive a {@link StreamClosedByCallerException}.
*
* The difference between this method and {@link #interrupt()} is only relevant if
* automatic retries are enabled. In this case, if you are using the
* {@link #messages()} or {@link #anyEvents()} iterator to read events, calling
* {@link #interrupt()} will cause the stream to be closed and then immediately
* reconnected, whereas {@link #stop()} will close it and then the iterator
* will end. In either case, if you explicitly try to read another event it
* will start the stream again.
*
* If the stream is not currently active, calling this method has no effect.
*
* Note for Android developers: since it is generally undesirable to perform
* any network activity from the main thread, be aware that {@link #interrupt()},
* {@link #stop()}, and {@link #close()} all cause an immediate close of the
* connection (if any), which happens on the same thread that called the method.
*
* @since 4.0.0
* @see #interrupt()
* @see #start()
*/
public void stop() {
closeCurrentStream(true, true);
}
/**
* Permanently shuts down the EventSource.
*
* This is similar to {@link #stop()} except that it also releases any resources that
* the EventSource was maintaining in general, such as an HTTP connection pool. Do
* not try to use the EventSource after closing it.
*
* Note for Android developers: since it is generally undesirable to perform
* any network activity from the main thread, be aware that {@link #interrupt()},
* {@link #stop()}, and {@link #close()} all cause an immediate close of the
* connection (if any), which happens on the same thread that called the method.
*/
@Override
public void close() {
ReadyState currentState = readyState.getAndSet(SHUTDOWN);
if (currentState == SHUTDOWN) {
return;
}
closeCurrentStream(true, true);
try {
client.close();
} catch (IOException e) {}
}
/**
* Blocks until all underlying threads have terminated and resources have been released.
*
* @param timeout maximum time to wait for everything to shut down, in whatever time
* unit is specified by {@code timeUnit}
* @param timeUnit the time unit, or {@code TimeUnit.MILLISECONDS} if null
* @return {@code true} if all thread pools terminated within the specified timeout,
* {@code false} otherwise
* @throws InterruptedException if this thread is interrupted while blocking
*/
public boolean awaitClosed(long timeout, TimeUnit timeUnit) throws InterruptedException {
return client.awaitClosed(millisFromTimeUnit(timeout, timeUnit));
}
// Iterator implementation used by messages() and anyEvents()
private class IteratorImpl implements Iterator {
private final Class filterClass;
IteratorImpl(Class filterClass) {
this.filterClass = filterClass;
calledStop = false;
}
public boolean hasNext() {
while (true) {
if (nextEvent != null && filterClass.isAssignableFrom(nextEvent.getClass())) {
return true;
}
if (calledStop) {
calledStop = false;
return false;
}
try {
nextEvent = requireEvent();
} catch (StreamException e) {
return false;
}
}
}
public T next() {
while (nextEvent == null || !filterClass.isAssignableFrom(nextEvent.getClass()) && hasNext()) {}
@SuppressWarnings("unchecked")
T event = (T)nextEvent;
nextEvent = null;
return event;
}
}
private StreamEvent requireEvent() throws StreamException {
readingThread.set(Thread.currentThread());
try {
while (true) {
// Reading an event implies starting the stream if it isn't already started.
// We might also be restarting since we could have been interrupted at any time.
if (eventParser == null) {
FaultEvent faultEvent = tryStart(true);
return faultEvent == null ? new StartedEvent() : faultEvent;
}
StreamEvent event = eventParser.nextEvent();
if (event instanceof SetRetryDelayEvent) {
// SetRetryDelayEvent means the stream contained a "retry:" line. We don't
// surface this to the caller, we just apply the new delay and move on.
baseRetryDelayMillis = ((SetRetryDelayEvent)event).getRetryMillis();
resetRetryDelayStrategy();
continue;
}
if (event instanceof MessageEvent) {
MessageEvent me = (MessageEvent)event;
if (me.getLastEventId() != null) {
lastEventId = me.getLastEventId();
}
}
return event;
}
} catch (StreamException e) {
readyState.set(ReadyState.CLOSED);
if (deliberatelyClosedConnection) {
// If the stream was explicitly closed from another thread, that'll likely show up as
// an I/O error, but we don't want to report it as one.
e = new StreamClosedByCallerException();
deliberatelyClosedConnection = false;
}
disconnectedTime = System.currentTimeMillis();
closeCurrentStream(false, false);
eventParser = null;
computeReconnectDelay();
if (applyErrorStrategy(e) == ErrorStrategy.Action.CONTINUE) {
return new FaultEvent(e);
}
throw e;
}
}
private void resetRetryDelayStrategy() {
logger.debug("Resetting retry delay strategy to initial state");
currentRetryDelayStrategy = baseRetryDelayStrategy;
}
private ErrorStrategy.Action applyErrorStrategy(StreamException e) {
ErrorStrategy.Result errorStrategyResult = currentErrorStrategy.apply(e);
if (errorStrategyResult.getNext() != null) {
currentErrorStrategy = errorStrategyResult.getNext();
}
return errorStrategyResult.getAction();
}
private void computeReconnectDelay() {
if (retryDelayResetThresholdMillis > 0 && connectedTime != 0) {
long connectionDurationMillis = System.currentTimeMillis() - connectedTime;
if (connectionDurationMillis >= retryDelayResetThresholdMillis) {
resetRetryDelayStrategy();
}
}
RetryDelayStrategy.Result result =
currentRetryDelayStrategy.apply(baseRetryDelayMillis);
nextReconnectDelayMillis = result.getDelayMillis();
if (result.getNext() != null) {
currentRetryDelayStrategy = result.getNext();
}
}
private boolean closeCurrentStream(boolean deliberatelyInterrupted, boolean shouldStopIterating) {
Closeable oldConnectionCloser = this.connectionCloser.getAndSet(null);
Thread oldReadingThread = readingThread.getAndSet(null);
if (oldConnectionCloser == null && oldReadingThread == null) {
return false;
}
synchronized (sleepNotifier) { // this synchronization prevents a race condition in start()
if (deliberatelyInterrupted) {
this.deliberatelyClosedConnection = true;
}
if (shouldStopIterating) {
this.calledStop = true;
}
if (oldConnectionCloser != null) {
try {
oldConnectionCloser.close();
logger.debug("Closed request");
} catch (IOException e) {
logger.warn("Unexpected error when closing connection: {}", LogValues.exceptionSummary(e));
}
}
if (oldReadingThread == Thread.currentThread()) {
eventParser = null;
readyState.compareAndSet(ReadyState.OPEN, ReadyState.CLOSED);
readyState.compareAndSet(ReadyState.CONNECTING, ReadyState.CLOSED);
// If the current thread is not the reading thread, these fields will be updated the
// next time the reading thread tries to do a read.
}
sleepNotifier.notify(); // in case we're sleeping in a reconnect delay, wake us up
}
return true;
}
/**
* Builder for configuring {@link EventSource}.
*/
public static final class Builder {
private final ConnectStrategy connectStrategy; // final because it's mandatory, set at constructor time
private ErrorStrategy errorStrategy;
private RetryDelayStrategy retryDelayStrategy;
private long retryDelayMillis = DEFAULT_RETRY_DELAY_MILLIS;
private long retryDelayResetThresholdMillis = DEFAULT_RETRY_DELAY_RESET_THRESHOLD_MILLIS;
private String lastEventId;
private int readBufferSize = DEFAULT_READ_BUFFER_SIZE;
private LDLogger logger = null;
private boolean streamEventData;
private Set expectFields = null;
/**
* Creates a new builder, specifying how it will connect to a stream.
*
* The {@link ConnectStrategy} will handle all details of how to obtain an
* input stream for the EventSource to consume. By default, this is
* {@link HttpConnectStrategy}, which makes HTTP requests. To customize the
* HTTP behavior, you can use methods of {@link HttpConnectStrategy}:
*
* EventSource.Builder builder = new EventSource.Builder(
* ConnectStrategy.http(myStreamUri)
* .headers(myCustomHeaders)
* .connectTimeout(10, TimeUnit.SECONDS)
* );
*
*
* Or, if you want to consume an input stream from some other source, you can
* create your own subclass of {@link ConnectStrategy}.
*
* @param connectStrategy the object that will manage the input stream;
* must not be null
* @since 4.0.0
* @see #Builder(URI)
* @see #Builder(HttpUrl)
* @throws IllegalArgumentException if the argument is null
*/
public Builder(ConnectStrategy connectStrategy) {
if (connectStrategy == null) {
throw new IllegalArgumentException("connectStrategy must not be null");
}
this.connectStrategy = connectStrategy;
}
/**
* Creates a new builder that connects via HTTP, specifying only the stream URI.
*
* Use this method if you do not need to configure any HTTP-related properties
* besides the URI. To specify a custom HTTP configuration instead, use
* {@link #Builder(ConnectStrategy)} with {@link ConnectStrategy#http(URI)}.
*
* @param uri the stream URI
* @throws IllegalArgumentException if the argument is null, or if the endpoint
* is not HTTP or HTTPS
* @see #Builder(ConnectStrategy)
* @see #Builder(URL)
* @see #Builder(HttpUrl)
*/
public Builder(URI uri) {
this(ConnectStrategy.http(uri));
}
/**
* Creates a new builder that connects via HTTP, specifying only the stream URI.
*
* This is the same as {@link #Builder(URI)}, but using the {@link URL} type.
*
* @param url the stream URL
* @throws IllegalArgumentException if the argument is null, or if the endpoint
* is not HTTP or HTTPS
* @see #Builder(ConnectStrategy)
* @see #Builder(URI)
* @see #Builder(HttpUrl)
*/
public Builder(URL url) {
this(ConnectStrategy.http(url));
}
/**
* Creates a new builder that connects via HTTP, specifying only the stream URI.
*
* This is the same as {@link #Builder(URI)}, but using the OkHttp type
* {@link HttpUrl}.
*
* @param url the stream URL
* @throws IllegalArgumentException if the argument is null, or if the endpoint
* is not HTTP or HTTPS
*
* @since 1.9.0
* @see #Builder(ConnectStrategy)
* @see #Builder(URI)
* @see #Builder(URL)
*/
public Builder(HttpUrl url) {
this(ConnectStrategy.http(url));
}
/**
* Specifies a strategy for determining whether to handle errors transparently
* or throw them as exceptions.
*
* By default, any failed connection attempt, or failure of an existing connection,
* will be thrown as a {@link StreamException} when you try to use the stream. You
* may instead use alternate {@link ErrorStrategy} implementations, such as
* {@link ErrorStrategy#alwaysContinue()}, or a custom implementation, to allow
* EventSource to continue after an error.
*
* @param errorStrategy the object that will control error handling; if null,
* defaults to {@link ErrorStrategy#alwaysThrow()}
* @return the builder
* @since 4.0.0
*/
public Builder errorStrategy(ErrorStrategy errorStrategy) {
this.errorStrategy = errorStrategy;
return this;
}
/**
* Sets the ID value of the last event received.
*
* This will be sent to the remote server on the initial connection request, allowing the server to
* skip past previously sent events if it supports this behavior. Once the connection is established,
* this value will be updated whenever an event is received that has an ID. Whether event IDs are
* supported depends on the server; it may ignore this value.
*
* @param lastEventId the last event identifier
* @return the builder
* @since 2.0.0
*/
public Builder lastEventId(String lastEventId) {
this.lastEventId = lastEventId;
return this;
}
/**
* Sets the base delay between connection attempts.
*
* The actual delay may be slightly less or greater, depending on the strategy specified by
* {@link #retryDelayStrategy(RetryDelayStrategy)}. The default behavior is to increase the
* delay exponentially from this base value on each attempt, up to a configured maximum,
* substracting a random jitter; for more details, see {@link DefaultRetryDelayStrategy}.
*
* If you set the base delay to zero, the backoff logic will not apply-- multiplying by
* zero gives zero every time. Therefore, use a zero delay with caution since it could
* cause a reconnect storm during a service interruption.
*
* @param retryDelay the base delay, in whatever time unit is specified by {@code timeUnit}
* @param timeUnit the time unit, or {@code TimeUnit.MILLISECONDS} if null
* @return the builder
* @see EventSource#DEFAULT_RETRY_DELAY_MILLIS
* @see #retryDelayStrategy(RetryDelayStrategy)
* @see #retryDelayResetThreshold(long, TimeUnit)
*/
public Builder retryDelay(long retryDelay, TimeUnit timeUnit) {
retryDelayMillis = millisFromTimeUnit(retryDelay, timeUnit);
return this;
}
/**
* Specifies a strategy for determining the retry delay after an error.
*
* Whenever EventSource tries to start a new connection after a stream failure,
* it delays for an amount of time that is determined by two parameters: the
* base retry delay ({@link #retryDelay(long, TimeUnit)}), and the retry delay
* strategy which transforms the base retry delay in some way. The default behavior
* is to apply an exponential backoff and jitter. You may instead use a modified
* version of {@link DefaultRetryDelayStrategy} to customize the backoff and
* jitter, or a custom implementation with any other logic.
*
* @param retryDelayStrategy the object that will control retry delays; if null,
* defaults to {@link RetryDelayStrategy#defaultStrategy()}
* @return the builder
* @see #retryDelay(long, TimeUnit)
* @see #retryDelayResetThreshold(long, TimeUnit)
* @since 4.0.0
*/
public Builder retryDelayStrategy(RetryDelayStrategy retryDelayStrategy) {
this.retryDelayStrategy = retryDelayStrategy;
return this;
}
/**
* Sets the minimum amount of time that a connection must stay open before the EventSource resets
* its delay strategy.
*
* When using the default strategy ({@link RetryDelayStrategy#defaultStrategy()}), this means that
* the delay before each reconnect attempt will be greater than the last delay unless the current
* connection lasted longer than the threshold, in which case the delay will start over at the
* initial minimum value. This prevents long delays from occurring on connections that are only
* rarely restarted.
*
* @param retryDelayResetThreshold the minimum time that a connection must stay open to avoid resetting
* the delay, in whatever time unit is specified by {@code timeUnit}
* @param timeUnit the time unit, or {@code TimeUnit.MILLISECONDS} if null
* @return the builder
* @see EventSource#DEFAULT_RETRY_DELAY_RESET_THRESHOLD_MILLIS
* @since 4.0.0
*/
public Builder retryDelayResetThreshold(long retryDelayResetThreshold, TimeUnit timeUnit) {
this.retryDelayResetThresholdMillis = millisFromTimeUnit(retryDelayResetThreshold, timeUnit);
return this;
}
/**
* Specifies the fixed size of the buffer that EventSource uses to parse incoming data.
*
* EventSource allocates a single buffer to hold data from the stream as it scans for
* line breaks. If no lines of data from the stream exceed this size, it will keep reusing
* the same space; if a line is longer than this size, it creates a temporary
* {@code ByteArrayOutputStream} to accumulate data for that line, which is less efficient.
* Therefore, if an application expects to see many lines in the stream that are longer
* than {@link EventSource#DEFAULT_READ_BUFFER_SIZE}, it can specify a larger buffer size
* to avoid unnecessary heap allocations.
*
* @param readBufferSize the buffer size
* @return the builder
* @throws IllegalArgumentException if the size is less than or equal to zero
* @see EventSource#DEFAULT_READ_BUFFER_SIZE
* @since 2.4.0
*/
public Builder readBufferSize(int readBufferSize) {
if (readBufferSize <= 0) {
throw new IllegalArgumentException("readBufferSize must be greater than zero");
}
this.readBufferSize = readBufferSize;
return this;
}
/**
* Specifies a custom logger to receive EventSource logging.
*
* This method uses the {@link LDLogger} type from
* com.launchdarkly.logging, a
* facade that provides several logging implementations as well as the option to forward
* log output to SLF4J or another framework. Here is an example of configuring it to use
* the basic console logging implementation, and to tag the output with the name "logname":
*
* // import com.launchdarkly.logging.*;
*
* builder.logger(
* LDLogger.withAdapter(Logs.basic(), "logname")
* );
*
*
* If you do not provide a logger, the default is there is no log output.
*
* @param logger an {@link LDLogger} implementation, or null for no logging
* @return the builder
* @since 2.7.0
*/
public Builder logger(LDLogger logger) {
this.logger = logger;
return this;
}
/**
* Specifies whether EventSource should send a {@link MessageEvent} to the handler as soon as it receives the
* beginning of the event data, allowing the handler to read the data incrementally with
* {@link MessageEvent#getDataReader()}.
*
* The default for this property is {@code false}, meaning that EventSource will always read the entire event into
* memory before dispatching it to the handler.
*
* If you set it to {@code true}, it will instead call the handler as soon as it sees a {@code data} field--
* setting {@link MessageEvent#getDataReader()} to a {@link java.io.Reader} that reads directly from the data as
* it arrives. The EventSource will perform any necessary parsing under the covers, so that for instance if there
* are multiple {@code data:} lines in the event, the Reader will emit a newline character between
* each and will not see the "data:" field names. The Reader will report "end of stream" as soon
* as the event is terminated normally by a blank line. If the stream is closed before normal termination of
* the event, the Reader will throw a {@link StreamClosedWithIncompleteMessageException}.
*
* This mode is designed for applications that expect very large data items to be delivered over SSE. Use it
* with caution, since there are several limitations:
*
* - The {@link MessageEvent} is constructed as soon as a {@code data:} field appears, so it will only include
* fields that appeared before {@code data:}. In other words, if the SSE server happens to send {@code data:}
* first and {@code event:} second, {@link MessageEvent#getEventName()} will not contain the value of
* {@code event:} but will be {@link MessageEvent#DEFAULT_EVENT_NAME} instead; similarly, an {@code id:} field will
* be ignored if it appears after {@code data:} in this mode. Therefore, you should only use this mode if the
* server's behavior is predictable in this regard.
* - The SSE protocol specifies that an event should be processed only if it is terminated by a blank line, but
* in this mode the handler will receive the event as soon as a {@code data:} field appears-- so, if the stream
* happens to cut off abnormally without a trailing blank line, technically you will be receiving an incomplete
* event that should have been ignored. You will know this has happened ifbecause reading from the Reader throws
* a {@link StreamClosedWithIncompleteMessageException}.
*
*
* @param streamEventData true if events should be dispatched immediately with asynchronous data rather than
* read fully before dispatch
* @return the builder
* @see #expectFields(String...)
* @since 2.6.0
*/
public Builder streamEventData(boolean streamEventData) {
this.streamEventData = streamEventData;
return this;
}
/**
* Specifies that the application expects the server to send certain fields in every event.
*
* This setting makes no difference unless you have enabled {@link #streamEventData(boolean)} mode. In that case,
* it causes EventSource to only use the streaming data mode for an event if the specified fields have
* already been received; otherwise, it will buffer the whole event (as if {@link #streamEventData(boolean)} had
* not been enabled), to ensure that those fields are not lost if they appear after the {@code data:} field.
*
* For instance, if you had called {@code expectFields("event")}, then EventSource would be able to use streaming
* data mode for the following SSE response--
*
* event: hello
* data: here is some very long streaming data
*
*
* --but it would buffer the full event if the server used the opposite order:
*
* data: here is some very long streaming data
* event: hello
*
*
* Such behavior is not automatic because in some applications, there might never be an {@code event:} field,
* and EventSource has no way to anticipate this.
*
* @param fieldNames a list of SSE field names (case-sensitive; any names other than "event" and "id" are ignored)
* @return the builder
* @see #streamEventData(boolean)
* @since 2.6.0
*/
public Builder expectFields(String... fieldNames) {
if (fieldNames == null || fieldNames.length == 0) {
expectFields = null;
} else {
expectFields = new HashSet<>();
for (String f: fieldNames) {
if (f != null) {
expectFields.add(f);
}
}
}
return this;
}
/**
* Constructs an {@link EventSource} using the builder's current properties.
* @return the new EventSource instance
*/
public EventSource build() {
return new EventSource(this);
}
}
}