Skip to content

Commit d3dba83

Browse files
LaunchDarklyReleaseBoteli-darklyLaunchDarklyCIgwhelanLDLaunchDarklyReleaseBot
authored
prepare 4.0.0 release (#80)
* add time threshold for backoff reset * allow endpoint to be specified as either URI or HttpUrl * add @SInCE * add interface for customizing requests * javadoc fixes * add changelog for past releases * remove JSR305 * replace SSL-specific config method with general-purpose HTTP config method * make helper method static * add end-to-end EventSource tests * spacing * omit default header value if there's a custom value * avoid trailing period in logger name * add 1.x branch * update to OkHttp 4.x and Java 8 * javadoc fixes * remove EventSource setters, + test improvements * update Gradle release * enable Github Pages * skip tests in release * add ability to force a stream restart; improve tests so we can test this * revert whitespace change * bump OkHttp version to latest Java 7-compatible version * add ability to force a stream restart; improve tests so we can test this (#29) * update to okhttp 4.5.0 * longer timeout for cleaner shutdown of test servers * fix Gradle scopes * allow setting specific thread priority * remove misleading logging & unnecessary backoff, improve tests (#34) * known issue with onClose() - add comment, disable test assertions * allow caller to specify a custom logger instead of SLF4J (#32) * add method for changing base name of SLF4J logger * enable coverage reports in CI, improve CI to test all supported Java versions * rm inapplicable CI copy-paste * another CI fix * add checkstyle config * fix jitter calculation when upper bound is a power of 2 * misc coverage + test improvements, add CI enforcement of coverage (#39) * fix shutdown state logic, simplify code paths (#40) * Fix Java 7 compatibility. * add OpenJDK 7 build + fix test race condition + javadoc fix (#42) * update Gradle to 6.8.3 * Kotlinize build script * fix logic for shutting down after an unrecoverable error * use newer HTTP test helpers * use Releaser v2 config + newer CI images (#47) * use new stream-reading implementation to support CR-only line endings * make buffer size configurable * rm usage that's not allowed in Java 8 * add Guava test dependency * add code coverage ovverride * implement contract tests (#48) * use Gradle 7 * Bounded queues for the EventHandler thread (#58) * Bounded queue for the EventHandler thread The unbounded queue fronting the 'event' thread can cause trouble when the EventHandler is unable to keep up with the workload. This can lead to heap exhaustion, GC issues and failure modes that are generally considered "bad". Band-aid over this with a semaphore to limit the number of tasks in the queue. The semaphore is opt-in and disabled by default to avoid any nasty surprises for folks upgrading. Also add 'EventSource.awaitClosed()' to allow users to wait for underlying thread pools to completely shut down. We can't know if it's safe to clean up resources used by the EventHandler thread if we can't be certain that it has completely terminated. * Address checkstyle griping in StubServer * Fix JavaDoc issue * Tighten up exception handling Co-authored-by: Eli Bishop <[email protected]> * update @SInCE * test Java 17 in CI (#51) * improve tests for AsyncEventHandler and EventSource.awaitClosed (#52) * add streaming data mode for very large events (#53) * add option to ensure that expected fields are always read * add Gradle option to suppress kotlin-stdlib in our pom * update okhttp to 4.9.3 * use LaunchDarkly logging facade * rm unused * misc fixes * improve javadoc links * remove SLF4J dependency, use only com.launchdarkly.logging * update com.launchdarkly.logging version * consistently use placeholders instead of concatenation in log output * update release metadata * use SecureRandom instead of Random, just to make scanners happier * use SecureRandom instead of Random, just to make scanners happier * use SecureRandom instead of Random, just to make scanners happier * fix release metadata * remove usage of Duration for Android compatibility * new synchronous EventSource implementation (#64) * add async wrapper to emulate old EventSource (#65) Co-authored-by: Eli Bishop <[email protected]> Co-authored-by: LaunchDarklyCI <[email protected]> Co-authored-by: Gavin Whelan <[email protected]> Co-authored-by: LaunchDarklyReleaseBot <[email protected]> Co-authored-by: Tom Lee <[email protected]>
1 parent 0110f1b commit d3dba83

File tree

60 files changed

+6773
-2904
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+6773
-2904
lines changed

build.gradle.kts

+12-10
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ dependencies {
6969
api("com.launchdarkly:launchdarkly-logging:${Versions.launchdarklyLogging}")
7070
api("com.squareup.okhttp3:okhttp:${Versions.okhttp}")
7171
testImplementation("org.mockito:mockito-core:1.10.19")
72-
testImplementation("com.launchdarkly:test-helpers:1.0.0")
72+
testImplementation("com.launchdarkly:test-helpers:2.0.1")
7373
testImplementation("com.google.guava:guava:30.1-jre")
7474
testImplementation("junit:junit:4.12")
7575
testImplementation("org.hamcrest:hamcrest-all:1.3")
@@ -122,24 +122,26 @@ tasks.jacocoTestCoverageVerification.configure {
122122
violationRules {
123123
val knownMissedLinesForMethods = mapOf(
124124
// The key for each of these items is the complete method signature minus the "com.launchdarkly.eventsource." prefix.
125-
"AsyncEventHandler.acquire()" to 2,
126-
"AsyncEventHandler.execute(java.lang.Runnable)" to 3,
125+
"EventParser.IncrementalMessageDataInputStream.read()" to 7,
126+
"EventParser.IncrementalMessageDataInputStream.read(byte[])" to 1,
127+
"EventParser.IncrementalMessageDataInputStream.read(byte[], int, int)" to 2,
128+
"EventParser.IncrementalMessageDataInputStream.canGetNextChunk()" to 3,
127129
"EventSource.awaitClosed(long, java.util.concurrent.TimeUnit)" to 2,
128130
"EventSource.awaitClosed(java.time.Duration)" to 1,
129-
"EventSource.handleSuccessfulResponse(okhttp3.Response)" to 2,
130-
"EventSource.maybeReconnectDelay(int, long)" to 2,
131-
"EventSource.run()" to 3,
132-
"EventSource.Builder.createInitialClientBuilder()" to 1,
133-
"EventSource.Builder.defaultTrustManager()" to 2,
131+
"Helpers.utf8ByteArrayOutputStreamToString(java.io.ByteArrayOutputStream)" to 2,
132+
"HttpConnectStrategy.defaultTrustManager()" to 2,
133+
"HttpConnectStrategy.Client.awaitClosed(long)" to 1,
134+
"HttpConnectStrategy.Client.createHttpClient()" to 2,
134135
"MessageEvent.getData()" to 2,
135136
"ModernTLSSocketFactory.createSocket(java.lang.String, int)" to 1,
136137
"ModernTLSSocketFactory.createSocket(java.lang.String, int, java.net.InetAddress, int)" to 1,
137138
"ModernTLSSocketFactory.createSocket(java.net.InetAddress, int)" to 1,
138139
"ModernTLSSocketFactory.createSocket(java.net.InetAddress, int, java.net.InetAddress, int)" to 1,
139140
"ModernTLSSocketFactory.createSocket(java.net.Socket, java.lang.String, int, boolean)" to 1,
140141
"ModernTLSSocketFactory.getDefaultCipherSuites()" to 1,
141-
"ModernTLSSocketFactory.getSupportedCipherSuites()" to 1
142-
)
142+
"ModernTLSSocketFactory.getSupportedCipherSuites()" to 1,
143+
"background.BackgroundEventSource.dispatchEvent(com.launchdarkly.eventsource.StreamEvent)" to 3
144+
)
143145

144146
knownMissedLinesForMethods.forEach { (signature, maxMissedLines) ->
145147
if (maxMissedLines > 0) { // < 0 means skip entire method

contract-tests/service/src/main/java/ssetest/StreamEntity.java

+46-32
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
import okhttp3.*;
99
import ssetest.Representations.*;
1010

11-
public class StreamEntity implements EventHandler {
11+
public class StreamEntity {
1212
private final TestService owner;
1313
private final String id;
14-
private final EventSource stream;
14+
private final EventSource eventSource;
1515
private final StreamOptions options;
1616
private final AtomicInteger callbackMessageCounter = new AtomicInteger(0);
1717
private final LDLogger logger;
@@ -25,74 +25,88 @@ public StreamEntity(TestService owner, String id, StreamOptions options, LDLogAd
2525
this.logger = LDLogger.withAdapter(logAdapter, options.tag);
2626
logger.info("Opening stream to {}", options.streamUrl);
2727

28-
EventSource.Builder eb = new EventSource.Builder(this, URI.create(options.streamUrl))
29-
.logger(logger.subLogger("stream"));
28+
HttpConnectStrategy connectStrategy = ConnectStrategy.http(URI.create(options.streamUrl));
29+
if (options.readTimeoutMs != null) {
30+
connectStrategy = connectStrategy.readTimeout((long)options.readTimeoutMs, null);
31+
}
32+
if (options.method != null) {
33+
connectStrategy = connectStrategy.methodAndBody(options.method,
34+
options.body == null ? null :
35+
RequestBody.create(options.body,
36+
MediaType.parse(options.headers.get("content-type") == null ?
37+
"text/plain; charset=utf-8" : options.headers.get("content-type")))
38+
);
39+
}
3040
if (options.headers != null) {
31-
Headers.Builder hb = new Headers.Builder();
3241
for (String name: options.headers.keySet()) {
33-
hb.add(name, options.headers.get(name));
42+
connectStrategy = connectStrategy.header(name, options.headers.get(name));
3443
}
35-
eb.headers(hb.build());
3644
}
45+
46+
EventSource.Builder eb = new EventSource.Builder(connectStrategy)
47+
.errorStrategy(ErrorStrategy.alwaysContinue())
48+
.logger(logger.subLogger("stream"));
3749
if (options.initialDelayMs != null) {
38-
eb.reconnectTime(options.initialDelayMs, null);
39-
}
40-
if (options.readTimeoutMs != null) {
41-
eb.readTimeout(options.readTimeoutMs, null);
50+
eb.retryDelay((long)options.initialDelayMs, null);
4251
}
4352
if (options.lastEventId != null) {
4453
eb.lastEventId(options.lastEventId);
4554
}
46-
if (options.method != null) {
47-
eb.method(options.method);
48-
}
49-
if (options.body != null) {
50-
String contentType = options.headers == null ? null : options.headers.get("content-type");
51-
eb.body(RequestBody.create(options.body,
52-
MediaType.parse(contentType == null ? "text/plain; charset=utf-8" : contentType)));
53-
}
54-
this.stream = eb.build();
55-
56-
this.stream.start();
55+
56+
this.eventSource = eb.build();
57+
new Thread(() -> {
58+
for (StreamEvent event: this.eventSource.anyEvents()) {
59+
handleEvent(event);
60+
}
61+
}).start();
5762
}
5863

5964
public boolean doCommand(String command) {
6065
logger.info("Test harness sent command: {}", command);
6166
if (command.equals("restart")) {
62-
stream.restart();
67+
eventSource.interrupt();
6368
return true;
6469
}
6570
return false;
6671
}
6772

6873
public void close() {
6974
closed = true;
70-
stream.close();
75+
eventSource.close();
7176
owner.forgetStream(id);
7277
logger.info("Test ended");
7378
}
7479

75-
public void onOpen() {}
76-
77-
public void onClosed() {}
80+
private void handleEvent(StreamEvent event) {
81+
if (event instanceof MessageEvent) {
82+
onMessage((MessageEvent)event);
83+
} else if (event instanceof CommentEvent) {
84+
onComment(((CommentEvent)event).getText());
85+
} else if (event instanceof FaultEvent) {
86+
onError(((FaultEvent)event).getCause());
87+
}
88+
}
7889

79-
public void onMessage(String name, MessageEvent e) {
80-
logger.info("Received event from stream ({})", name);
90+
private void onMessage(MessageEvent e) {
91+
logger.info("Received event from stream ({})", e.getEventName());
8192
Message m = new Message("event");
8293
m.event = new EventMessage();
83-
m.event.type = name;
94+
m.event.type = e.getEventName();
8495
m.event.data = e.getData();
8596
m.event.id = e.getLastEventId();
8697
writeMessage(m);
8798
}
8899

89-
public void onComment(String comment) {
100+
private void onComment(String comment) {
90101
Message m = new Message("comment");
91102
m.comment = comment;
92103
writeMessage(m);
93104
}
94105

95-
public void onError(Throwable t) {
106+
private void onError(Throwable t) {
107+
if (t instanceof StreamClosedByCallerException) {
108+
return; // the SSE contract tests don't want to see this non-error
109+
}
96110
logger.info("Received error from stream: {}", t.toString());
97111
Message m = new Message("error");
98112
m.error = t.toString();

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=3.0.0
1+
version=4.0.0-SNAPSHOT
22
ossrhUsername=
33
ossrhPassword=
44

src/main/java/com/launchdarkly/eventsource/AsyncEventHandler.java

-126
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.launchdarkly.eventsource;
2+
3+
import java.util.Objects;
4+
5+
/**
6+
* Describes a comment line received from the stream.
7+
* <p>
8+
* An SSE comment is a line that starts with a colon. There is no defined meaning for this
9+
* in the SSE specification, and most clients ignore it. It may be used to provide a
10+
* periodic heartbeat from the server to keep connections from timing out.
11+
*
12+
* @since 4.0.0
13+
*/
14+
public final class CommentEvent implements StreamEvent {
15+
private final String text;
16+
17+
/**
18+
* Creates an instance.
19+
*
20+
* @param text the comment text, not including the leading colon
21+
*/
22+
public CommentEvent(String text) {
23+
this.text = text;
24+
}
25+
26+
/**
27+
* Returns the comment text, not including the leading colon.
28+
*
29+
* @return the text
30+
*/
31+
public String getText() {
32+
return text;
33+
}
34+
35+
@Override
36+
public boolean equals(Object o) {
37+
if (this == o) return true;
38+
if (o == null || getClass() != o.getClass()) return false;
39+
40+
CommentEvent that = (CommentEvent) o;
41+
return Objects.equals(text, that.text);
42+
}
43+
44+
@Override
45+
public int hashCode() {
46+
return Objects.hash(text);
47+
}
48+
49+
@Override
50+
public String toString() {
51+
return "CommentEvent(" + text + ")";
52+
}
53+
}

0 commit comments

Comments
 (0)