diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java index 82b8de8d7..0a2dd3b6f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java @@ -118,8 +118,9 @@ EventProcessorBuilderImpl retryDelaysForTesting(long initialDelayMillis, long no return this; } - // How long events give way to flag values, settable by tests the specified waits would slow. - private DirectEventProcessor.Pacing pacing = DirectEventProcessor.Pacing.SPECIFIED; + // How long events give way to flag values, settable by tests the specified waits would slow. Null means + // the configured delivery delay and the specified gap. + private DirectEventProcessor.Pacing pacing; EventProcessorBuilderImpl pacingForTesting(DirectEventProcessor.Pacing pacing) { this.pacing = pacing; @@ -164,7 +165,7 @@ public EventProcessor build(ClientContext clientContext) { flushIntervalMillis, diagnosticRecordingIntervalMillis, DirectEventProcessor.DEFAULT_CLOSE_BUDGET_MILLIS, - pacing, + pacing != null ? pacing : DirectEventProcessor.Pacing.specified(eventDeliveryDelayMillis), clientContext.isInBackground(), true, // initiallyOffline EventUtil.makeEventsTaskExecutor(), diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java index 7126c0f94..fc4ceab1f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java @@ -43,22 +43,28 @@ * one stored batch. They send it under the same payload ID, and LaunchDarkly discards the repeat rather than * counting the events twice. *

- * Flag values come first. Where events persist, what a previous run left waits at launch until a fixed time - * after the SDK first goes online, and the deliveries this processor starts on its own send one batch at a time, + * Flag values come first. What this processor sends on its own waits at launch until + * {@link com.launchdarkly.sdk.android.integrations.EventProcessorBuilder#eventDeliveryDelayMillis(long)} after the + * SDK first goes online, and where events persist, the deliveries it starts on its own send one batch at a time, * a gap apart, leaving room for flag requests. Both go by time alone: nothing here hears what the flag data * source is doing. A delivery the application asks for is never held. */ final class DirectEventProcessor implements EventProcessor { /** How long the deliveries this processor starts on its own wait, so that flag values come first. */ static final class Pacing { - static final Pacing SPECIFIED = new Pacing(2_000, 1_000); + /** The gap the specification gives, after the launch delay the application configured. */ + static Pacing specified(long launchDelayMillis) { + return new Pacing(launchDelayMillis, 1_000); + } + /** No waits at all, for tests about something else. */ static final Pacing NONE = new Pacing(0, 0); /** - * How long after the SDK first goes online that a previous run's events, and the diagnostic init event, - * wait. It gives the application's startup traffic, the first flag request among it, the network first, - * and is short enough for an application that dies a few seconds after every launch to still send them. + * How long after the SDK first goes online that a previous run's events, this run's periodic delivery, + * and the diagnostic init event wait. It gives the application's startup traffic, the first flag request + * among it, the network first, and is short enough for an application that dies a few seconds after + * every launch to still send them. */ final long launchDelayMillis; /** The least time between one paced batch's answer and the next batch's request. */ @@ -138,16 +144,16 @@ static final class Pacing { private final Pacing pacing; /** - * Whether events give way to flag values: only where the application asked for persistence, like the - * rest of what persistence brings. Without it there is no previous run to hold back, and delivery is - * what it was before persistence: no launch gate and no pacing. + * Whether the deliveries this processor starts on its own are paced: only where the application asked for + * persistence, like the rest of what persistence brings. Without it there is no backlog from a previous + * run to spread out. The launch gate does not depend on it. */ private final boolean givesWayToFlags; /** * Whether the launch gate has opened. It opens once the launch delay has passed since the SDK first went - * online. Until then neither a previous run's events nor the diagnostic init event are sent. Open from the - * start where events do not give way. Only set on the scheduler thread. + * online. Until then neither a previous run's events, this run's periodic delivery, nor the diagnostic init + * event are sent. Open from the start where there is no launch delay. Only set on the scheduler thread. */ private volatile boolean launchGateOpen; @@ -270,7 +276,7 @@ private static final class ReadFailures { this.closeBudgetMillis = closeBudgetMillis; this.pacing = pacing; this.givesWayToFlags = store.isPersisting(); - this.launchGateOpen = !givesWayToFlags; + this.launchGateOpen = pacing.launchDelayMillis <= 0; this.scheduler = scheduler; this.logger = logger; this.committer = new EventCommitter(eventBuffer, store, capacity, commitOnCallerThread, commitExecutor, diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java index fa5d53b8f..503178159 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java @@ -61,6 +61,16 @@ public abstract class EventProcessorBuilder implements ComponentConfigurer + * That is the diagnostic init event, the events a previous run left where + * {@link #eventPersistence(EventPersistence)} keeps them, and the periodic delivery of this run's events. + * A flush the application asks for is never held. Zero sends them as soon as the SDK is online. + *

+ * This setting is experimental, and not subject to any backwards compatibility guarantees or semantic + * versioning. + *

+ * The default value is {@link #DEFAULT_EVENT_DELIVERY_DELAY_MILLIS}. A negative value is treated as zero, + * and a value above {@link #MAX_EVENT_DELIVERY_DELAY_MILLIS} as that maximum, so an application that ends + * soon after every launch still sends what a previous run left. + * + * @param eventDeliveryDelayMillis how long to hold back, in milliseconds + * @return the builder + */ + public EventProcessorBuilder eventDeliveryDelayMillis(long eventDeliveryDelayMillis) { + this.eventDeliveryDelayMillis = Math.min(Math.max(eventDeliveryDelayMillis, 0), + MAX_EVENT_DELIVERY_DELAY_MILLIS); + return this; + } + /** * Marks a set of attribute names or subproperties as private. *

diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java index 0489e132f..0b378e53c 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java @@ -3801,14 +3801,47 @@ public void goingOfflineStopsTheRetryOfAFailedPost() throws Exception { } @Test - public void theDiagnosticInitEventWaitsForTheLaunchGateOnlyWhereEventsPersist() throws Exception { + public void theDiagnosticInitEventWaitsForTheLaunchGateWhetherOrNotEventsPersist() throws Exception { try (HttpServer server = startEventsServer()) { EventProcessor eventProcessor = nextRunOffline(server, new DirectEventProcessor.Pacing(1_000, 0), false); try { eventProcessor.setOffline(false); - if (persistence() != EventPersistence.DISABLED) { - server.getRecorder().requireNoRequests(600, TimeUnit.MILLISECONDS); - } + server.getRecorder().requireNoRequests(600, TimeUnit.MILLISECONDS); + + assertEquals("/mobile/events/diagnostic", + server.getRecorder().requireRequest(2, TimeUnit.SECONDS).getPath()); + } finally { + eventProcessor.close(); + } + } + } + + @Test + public void theDiagnosticInitEventGoesOutAtOnceWithNoLaunchDelay() throws Exception { + try (HttpServer server = startEventsServer()) { + EventProcessor eventProcessor = nextRunOffline(server, DirectEventProcessor.Pacing.NONE, false); + try { + eventProcessor.setOffline(false); + + assertEquals("/mobile/events/diagnostic", + server.getRecorder().requireRequest(500, TimeUnit.MILLISECONDS).getPath()); + } finally { + eventProcessor.close(); + } + } + } + + @Test + public void theLaunchDelayIsTheConfiguredEventDeliveryDelay() throws Exception { + try (HttpServer server = startEventsServer()) { + EventProcessor eventProcessor = buildOfflineEventProcessor(server, + ((ComponentsImpl.EventProcessorBuilderImpl) eventsBuilder(DEFAULT_CAPACITY) + .eventDeliveryDelayMillis(1_000)) + .pacingForTesting(null), + false); + try { + eventProcessor.setOffline(false); + server.getRecorder().requireNoRequests(600, TimeUnit.MILLISECONDS); assertEquals("/mobile/events/diagnostic", server.getRecorder().requireRequest(2, TimeUnit.SECONDS).getPath()); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilderTest.java new file mode 100644 index 000000000..6c5ec6837 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilderTest.java @@ -0,0 +1,24 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.android.Components; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class EventProcessorBuilderTest { + @Test + public void eventDeliveryDelayDefaultsToTwoSeconds() { + assertEquals(2_000, Components.sendEvents().eventDeliveryDelayMillis); + } + + @Test + public void eventDeliveryDelayIsKeptBetweenZeroAndAMinute() { + EventProcessorBuilder builder = Components.sendEvents(); + assertEquals(0, builder.eventDeliveryDelayMillis(0).eventDeliveryDelayMillis); + assertEquals(5_000, builder.eventDeliveryDelayMillis(5_000).eventDeliveryDelayMillis); + assertEquals(0, builder.eventDeliveryDelayMillis(-1).eventDeliveryDelayMillis); + assertEquals(60_000, builder.eventDeliveryDelayMillis(61_000).eventDeliveryDelayMillis); + assertEquals(60_000, builder.eventDeliveryDelayMillis(Long.MAX_VALUE).eventDeliveryDelayMillis); + } +}