Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0c98c92
feat(events): add a bounded flush (tier 2)
abelonogov-ld Sep 4, 2026
d57313c
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 15, 2026
818d239
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 15, 2026
63ca732
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 15, 2026
d399f54
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 17, 2026
ef886dc
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 18, 2026
536a453
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 21, 2026
19a14b3
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 21, 2026
f233167
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 21, 2026
0bcef8b
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 21, 2026
644ac0f
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 21, 2026
68d8aa9
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
7bd8b07
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
7385975
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
f278820
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
c6386f7
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
f36aee4
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
23bba70
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
a408e75
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
113ee34
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
70babb2
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
ff5aeec
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
105c870
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
bdfd13a
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
6290c38
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 22, 2026
ee78359
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 23, 2026
c629046
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 23, 2026
2237489
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 23, 2026
df9e409
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 23, 2026
e113694
Report a failed flushAndWait from a closed client
abelonogov-ld Sep 23, 2026
4ac8572
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 23, 2026
7fe25ed
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Sep 28, 2026
6f1ae80
Flush through a future, shared by the callers who asked at once
abelonogov-ld Oct 2, 2026
3ab2ffc
Merge branch 'andrey/event-durability-tier1-buffer' into andrey/event…
abelonogov-ld Oct 2, 2026
90f4eed
change to 500 ms
abelonogov-ld Oct 6, 2026
14b9e47
Tell a flush caller about events an earlier delivery took and lost
abelonogov-ld Oct 6, 2026
6de55d5
Keep flushAndWait bounded at Long.MIN_VALUE and boolean on a cancelle…
abelonogov-ld Oct 6, 2026
0421534
fix
abelonogov-ld Oct 6, 2026
51bae97
Pin flushAndWait's outcome for refused posts, abandoned waits and par…
abelonogov-ld Oct 6, 2026
30a5008
Move the crash flush into the SDK as EventFlushingCrashHandler
abelonogov-ld Oct 6, 2026
f229dde
Rename EventFlushingCrashHandler to LDCrashHandler
abelonogov-ld Oct 6, 2026
b8fd94c
Pin the 503 outcomes Todd proposed, say refused events are not kept, …
abelonogov-ld Oct 7, 2026
7457a26
Say how long a failing delivery takes, so a shorter flushAndWait budg…
abelonogov-ld Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import com.launchdarkly.sdk.android.DataModel.Flag;
import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes;
import com.launchdarkly.sdk.android.integrations.DedupingHook;
import com.launchdarkly.sdk.android.integrations.LDCrashHandler;
import com.launchdarkly.sdk.android.integrations.Hook;
import com.launchdarkly.sdk.android.subsystems.EventProcessor;
import com.launchdarkly.sdk.android.subsystems.PersistentDataStore;
import com.launchdarkly.sdk.internal.GsonHelpers;
import com.launchdarkly.sdk.json.JsonSerialization;
Expand All @@ -26,8 +28,14 @@
import org.junit.Test;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

import okhttp3.HttpUrl;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
Expand Down Expand Up @@ -95,6 +103,165 @@ public void testTrackData() throws IOException, InterruptedException {
}
}

@Test
public void flushAndWaitReportsDelivery() throws IOException, InterruptedException {
try (MockWebServer mockEventsServer = new MockWebServer()) {
mockEventsServer.start();
mockEventsServer.enqueue(new MockResponse());

LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build();
try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) {
client.track("test-event");

assertTrue(client.flushAndWait(5, TimeUnit.SECONDS));
LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2);
assertCustomEvent(events[1], ldContext, "test-event");
}
}
}

@Test
public void crashHandlerDeliversTheEventsBeforePassingTheCrashOn() throws IOException, InterruptedException {
Thread.UncaughtExceptionHandler originalDefault = Thread.getDefaultUncaughtExceptionHandler();
try (MockWebServer mockEventsServer = new MockWebServer()) {
mockEventsServer.start();
mockEventsServer.enqueue(new MockResponse());

LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build();
try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) {
int[] requestsSeenByNextHandler = {-1};
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) ->
requestsSeenByNextHandler[0] = mockEventsServer.getRequestCount());
LDCrashHandler.install(5, TimeUnit.SECONDS);
client.track("test-event");

Thread.getDefaultUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), new RuntimeException("boom"));

assertEquals(1, requestsSeenByNextHandler[0]);
LDValue[] events = getEventsFromLastRequest(mockEventsServer, 2);
assertCustomEvent(events[1], ldContext, "test-event");
}
} finally {
Thread.setDefaultUncaughtExceptionHandler(originalDefault);
}
}

@Test
public void flushAndWaitReportsFailureOnceClosed() throws IOException {
try (MockWebServer mockEventsServer = new MockWebServer()) {
mockEventsServer.start();

LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build();
LDClient client = LDClient.init(application, ldConfig, ldContext, 0);
client.close();

assertFalse(client.flushAndWait(5, TimeUnit.SECONDS));
}
}

@Test
public void flushAndWaitDeliversEveryEnvironmentAtOnce() throws Exception {
// Each post is held for longer than half the budget, so the call can only report true if the
// environments' deliveries ran side by side rather than one after the other.
try (MockWebServer mockEventsServer = new MockWebServer()) {
mockEventsServer.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
return new MockResponse().setHeadersDelay(1_500, TimeUnit.MILLISECONDS);
}
});
mockEventsServer.start();

Map<String, String> secondaryKeys = new HashMap<>();
secondaryKeys.put("second", "second-mobile-key");
LDConfig ldConfig = baseConfigBuilder(mockEventsServer)
.secondaryMobileKeys(secondaryKeys)
.build();
try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) {
client.track("primary-event");
LDClient.getForMobileKey("second").track("second-event");

assertTrue(client.flushAndWait(2_500, TimeUnit.MILLISECONDS));
assertEquals(2, mockEventsServer.getRequestCount());
}
}
}

@Test
public void flushAndWaitWithTheMostNegativeTimeoutDoesNotWait() throws IOException {
// toNanos saturates at Long.MIN_VALUE, and unclamped that wraps round to a wait for as long
// as the delivery takes -- which would then be reported as delivered.
try (MockWebServer mockEventsServer = new MockWebServer()) {
mockEventsServer.start();
mockEventsServer.enqueue(new MockResponse().setHeadersDelay(3, TimeUnit.SECONDS));

LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build();
try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) {
client.track("test-event");

long started = System.nanoTime();
assertFalse(client.flushAndWait(Long.MIN_VALUE, TimeUnit.NANOSECONDS));
long waitedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
assertTrue("waited " + waitedMillis + "ms", waitedMillis < 1_000);
}
}
}

@Test
public void flushAndWaitReportsFailureWhenTheDeliveryIsCanceled() throws IOException {
// A custom event processor may hand back a future that is canceled; Future.get then throws
// CancellationException, which is unchecked and must not escape a boolean answer.
try (MockWebServer mockEventsServer = new MockWebServer()) {
mockEventsServer.start();

LDConfig ldConfig = baseConfigBuilder(mockEventsServer)
.events(clientContext -> new CancelingEventProcessor())
.build();
try (LDClient client = LDClient.init(application, ldConfig, ldContext, 0)) {
assertFalse(client.flushAndWait(5, TimeUnit.SECONDS));
}
}
}

/** An event processor whose deliveries are always canceled before they can report. */
private static final class CancelingEventProcessor implements EventProcessor {
@Override
public Future<Boolean> flushAsync() {
FutureTask<Boolean> delivery = new FutureTask<>(() -> true);
delivery.cancel(false);
return delivery;
}

@Override
public void flush() {}

@Override
public void blockingFlush() {}

@Override
public void setInBackground(boolean inBackground) {}

@Override
public void setOffline(boolean offline) {}

@Override
public void close() {}

@Override
public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion,
int variation, LDValue value, EvaluationReason reason,
LDValue defaultValue, boolean requireFullEvent,
Long debugEventsUntilDate) {}

@Override
public void recordIdentifyEvent(LDContext context) {}

@Override
public void recordCustomEvent(LDContext context, String eventKey, LDValue data,
Double metricValue) {}
}

@Test
public void testTrackDataValueNull() throws IOException, InterruptedException {
try (MockWebServer mockEventsServer = new MockWebServer()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;

/**
* This class contains the package-private implementations of component factories and builders whose
Expand Down Expand Up @@ -72,6 +73,12 @@ public void flush() {}
@Override
public void blockingFlush() {}

@Override
public Future<Boolean> flushAsync() {
// Nothing was recorded, so there is nothing undelivered to warn the caller about.
return new LDSuccessFuture<>(true);
}

@Override
public void setInBackground(boolean inBackground) {}

Expand Down
Loading
Loading