From 555c0acc36066425fa3f111ce9bbfc532590899a Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 6 Nov 2026 23:27:50 -0500 Subject: [PATCH] feat: adopt the event-delivery cooldown from java-sdk-internal 1.13.0 (SDK-3375) Picks up the internal events module release in which an unexpected delivery failure opens a bounded, self-recovering cooldown window instead of permanently disabling event delivery. The public EventSender.Result.STOP javadoc now describes the cooldown contract instead of a permanent stop; the enum itself is unchanged, so custom senders compile and run unmodified. The internal wrapper moves off the deprecated internal Result constructor onto the new factories, with new unit coverage for all three mappings and their routing. The contract test service advertises event-delivery-cooldown, which activates the delivery cooldown tests; the 413 variant is suppressed in both suppression files with an in-file rationale, since the shared HTTP classifier still treats a 413 as an unexpected failure rather than a locally unrecoverable one, which is a pre-existing gap tracked separately. --- lib/sdk/server/build.gradle | 2 +- .../src/main/java/sdktest/TestService.java | 1 + .../contract-tests/test-suppressions-fdv2.txt | 5 + .../contract-tests/test-suppressions.txt | 6 +- .../sdk/server/ComponentsImpl.java | 6 +- .../sdk/server/subsystems/EventSender.java | 15 ++- .../sdk/server/EventSenderWrapperTest.java | 109 ++++++++++++++++++ 7 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EventSenderWrapperTest.java diff --git a/lib/sdk/server/build.gradle b/lib/sdk/server/build.gradle index 67502ef8..49cc52a0 100644 --- a/lib/sdk/server/build.gradle +++ b/lib/sdk/server/build.gradle @@ -73,7 +73,7 @@ ext.versions = [ "jackson": "2.11.2", "jsr305": "3.0.2", "launchdarklyJavaSdkCommon": "2.3.0", - "launchdarklyJavaSdkInternal": "1.12.2", + "launchdarklyJavaSdkInternal": "1.13.0", "launchdarklyLogging": "1.1.0", "okhttp": "4.12.0", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource "okhttpEventsource": "5.0.1", diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java index cbc0fab9..78c2329a 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java @@ -32,6 +32,7 @@ public class TestService { "context-comparison", "context-type", "evaluation-hooks", + "event-delivery-cooldown", "event-gzip", "event-sampling", "filtering", diff --git a/lib/sdk/server/contract-tests/test-suppressions-fdv2.txt b/lib/sdk/server/contract-tests/test-suppressions-fdv2.txt index 9765a098..67388ff5 100644 --- a/lib/sdk/server/contract-tests/test-suppressions-fdv2.txt +++ b/lib/sdk/server/contract-tests/test-suppressions-fdv2.txt @@ -1,3 +1,8 @@ streaming/fdv2/recoverable fallback to secondary synchronizer streaming/fdv2/recoverable fallback with recovery streaming/fdv2/permanent fallback with recovery +# launchdarkly-java-sdk-internal's HttpErrors.classifyHttpFailure treats a 413 as an unexpected +# failure (it has no locally-unrecoverable classification), so a 413 opens a cooldown window +# instead of only dropping the oversized payload (SDK-3513 owns the fix and the removal of this +# suppression). +events/delivery cooldown/413 is dropped without disabling delivery diff --git a/lib/sdk/server/contract-tests/test-suppressions.txt b/lib/sdk/server/contract-tests/test-suppressions.txt index 8b137891..f2bb348b 100644 --- a/lib/sdk/server/contract-tests/test-suppressions.txt +++ b/lib/sdk/server/contract-tests/test-suppressions.txt @@ -1 +1,5 @@ - +# launchdarkly-java-sdk-internal's HttpErrors.classifyHttpFailure treats a 413 as an unexpected +# failure (it has no locally-unrecoverable classification), so a 413 opens a cooldown window +# instead of only dropping the oversized payload (SDK-3513 owns the fix and the removal of this +# suppression). +events/delivery cooldown/413 is dropped without disabling delivery diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java index f808ebf7..56c6eccd 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java @@ -299,11 +299,11 @@ public Result sendDiagnosticEvent(byte[] data, URI eventsBaseUri) { private Result transformResult(com.launchdarkly.sdk.server.subsystems.EventSender.Result result) { switch (result) { case FAILURE: - return new Result(false, false, null); + return Result.failure(); case STOP: - return new Result(false, true, null); + return Result.unexpectedFailure(); default: - return new Result(true, false, null); + return Result.success(null); } } } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventSender.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventSender.java index 20b4e29f..bac4725f 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventSender.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/EventSender.java @@ -29,8 +29,19 @@ public enum Result { FAILURE, /** - * The EventSender was not able to deliver the events, and the nature of the error indicates that - * the SDK should not attempt to send any more events. + * The EventSender was not able to deliver the events, and the nature of the error (such as an + * HTTP 401 error, implying that the SDK key is invalid) indicates that the SDK should stop + * attempting delivery for a while. + *

+ * The SDK responds by entering a bounded cooldown window. While the window is open, the SDK + * initiates no new analytics-event deliveries through this EventSender (a delivery already + * dispatched when the window opened still completes), and analytics events that would have + * been sent during the window are discarded rather than held. Diagnostic-event calls are not + * suppressed, and this result from one of them opens a window of its own. If delivery is + * still failing this way when a window elapses, the next window is longer, up to a ceiling; + * sustained successful delivery returns it to the initial length. Delivery resumes on its own + * once a window has elapsed; no application action is required, and event delivery is never + * disabled permanently. Returning {@link #FAILURE} instead never opens or lengthens a window. */ STOP }; diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EventSenderWrapperTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EventSenderWrapperTest.java new file mode 100644 index 00000000..8b3084c9 --- /dev/null +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EventSenderWrapperTest.java @@ -0,0 +1,109 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.server.ComponentsImpl.EventProcessorBuilderImpl.EventSenderWrapper; +import com.launchdarkly.sdk.server.subsystems.EventSender; + +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings("javadoc") +public class EventSenderWrapperTest { + private static final URI FAKE_URI = URI.create("http://fake"); + private static final byte[] FAKE_DATA = new byte[] {1, 2, 3}; + + private static final class RecordingSender implements EventSender { + private final Result result; + String methodCalled; + byte[] receivedData; + int receivedEventCount = -1; + URI receivedUri; + + RecordingSender(Result result) { + this.result = result; + } + + @Override + public Result sendAnalyticsEvents(byte[] data, int eventCount, URI eventsBaseUri) { + methodCalled = "analytics"; + receivedData = data; + receivedEventCount = eventCount; + receivedUri = eventsBaseUri; + return result; + } + + @Override + public Result sendDiagnosticEvent(byte[] data, URI eventsBaseUri) { + methodCalled = "diagnostic"; + receivedData = data; + receivedUri = eventsBaseUri; + return result; + } + + @Override + public void close() throws IOException {} + } + + @Test + public void successMapsToSuccessfulDelivery() { + RecordingSender sender = new RecordingSender(EventSender.Result.SUCCESS); + com.launchdarkly.sdk.internal.events.EventSender.Result result = + new EventSenderWrapper(sender).sendAnalyticsEvents(FAKE_DATA, 3, FAKE_URI); + + assertTrue(result.isSuccess()); + assertFalse(result.isUnexpectedFailure()); + assertNull(result.getTimeFromServer()); + + // The wrapper routes to the analytics method with the arguments unchanged. + assertEquals("analytics", sender.methodCalled); + assertSame(FAKE_DATA, sender.receivedData); + assertEquals(3, sender.receivedEventCount); + assertSame(FAKE_URI, sender.receivedUri); + } + + @Test + public void failureMapsToOrdinaryFailure() { + RecordingSender sender = new RecordingSender(EventSender.Result.FAILURE); + com.launchdarkly.sdk.internal.events.EventSender.Result result = + new EventSenderWrapper(sender).sendAnalyticsEvents(FAKE_DATA, 1, FAKE_URI); + + assertFalse(result.isSuccess()); + assertFalse(result.isUnexpectedFailure()); + } + + @SuppressWarnings("deprecation") + @Test + public void stopMapsToUnexpectedFailure() { + RecordingSender sender = new RecordingSender(EventSender.Result.STOP); + com.launchdarkly.sdk.internal.events.EventSender.Result result = + new EventSenderWrapper(sender).sendAnalyticsEvents(FAKE_DATA, 1, FAKE_URI); + + assertFalse(result.isSuccess()); + assertTrue(result.isUnexpectedFailure()); + // The legacy flag stays written for consumers that have not migrated off it. + assertTrue(result.isMustShutDown()); + } + + @Test + public void diagnosticPathUsesTheSameMappingAndRouting() { + RecordingSender stopSender = new RecordingSender(EventSender.Result.STOP); + com.launchdarkly.sdk.internal.events.EventSender.Result stopResult = + new EventSenderWrapper(stopSender).sendDiagnosticEvent(FAKE_DATA, FAKE_URI); + assertTrue(stopResult.isUnexpectedFailure()); + assertEquals("diagnostic", stopSender.methodCalled); + assertSame(FAKE_DATA, stopSender.receivedData); + assertSame(FAKE_URI, stopSender.receivedUri); + + RecordingSender successSender = new RecordingSender(EventSender.Result.SUCCESS); + assertTrue(new EventSenderWrapper(successSender) + .sendDiagnosticEvent(FAKE_DATA, FAKE_URI).isSuccess()); + assertEquals("diagnostic", successSender.methodCalled); + } +}