Skip to content

feat(events): add flushAndWait with a bounded timeout (tier 2) - #403

Open
abelonogov-ld wants to merge 31 commits into
andrey/event-durability-tier1-bufferfrom
andrey/event-durability-tier2-bounded-flush
Open

abelonogov-ld wants to merge 31 commits into
andrey/event-durability-tier1-bufferfrom
andrey/event-durability-tier2-bounded-flush

Conversation

@abelonogov-ld

@abelonogov-ld abelonogov-ld commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

Tier 2 of the Event Durability spec, "Tier 2 — recoverable failure" (§10). Stacked on #397 (tier 1), and targets that branch so this diff shows only tier 2. It is small and self-contained, so it can be reviewed in parallel with #397. It will be retargeted to main once #397 merges.

Describe the solution you've provided

A new public method, LDClientInterface.flushAndWait(long timeout, TimeUnit unit). It sends what has been recorded and returns whether the events were delivered within the timeout.

flush() is fire-and-forget, so an application that knows it is about to lose the chance to send (an uncaught exception handler, a move to the background) had no way to give its events that chance and find out whether it worked. flushAndWait gives it both, with a bound the caller chooses.

  • EventProcessor gains blockingFlush(long, TimeUnit) as a default method, so custom implementations written before it still compile. The default falls back to the unbounded blockingFlush() and returns true.
  • DirectEventProcessor implements it. Delivery now reports its outcome: true if the events were accepted or there was nothing to send, false if offline, closed, or the send failed. If the timeout expires, the delivery is left running rather than cancelled, because the buffer has already been drained into the payload and interrupting it would only make the loss certain.
  • With more than one environment configured, LDClient.flushAndWait shares one budget across them rather than giving each a fresh copy, so the timeout is the most the call can take.

Describe alternatives you've considered

Make flush() return a Future. That changes the signature of an existing public method, and a Future still leaves the caller to pick a timeout and interpret the exceptions. A boolean with the timeout in the call is the shape a crash handler actually needs.

Cancel the delivery on timeout. It frees the thread sooner but guarantees the events are lost, since they are no longer in the buffer. Letting it finish in the background gives them the best chance at no cost to the caller, who has already stopped waiting.

Additional context

Tests. Four new tests in DirectEventProcessorTest cover delivered events, nothing to send, offline, and the timeout expiring first. The full unit suite and the instrumented suite (126 tests on an emulator) pass locally.

Test app. Gains two controls that show what this tier can and cannot do:

  • Eval+Crash now records a flag evaluation and a custom event, then throws. FlushOnCrashHandler calls flushAndWait with a two-second budget before handing the crash on, so the events are delivered.
  • Eval+Kill now records the same pair and sends SIGKILL. Nothing can run, so the events are lost. That is the gap tier 3 (on-disk persistence) closes.

Version. The new methods are marked @since 5.17.0; adjust if the release lands under a different version.


Note

Overview
Adds flushAndWait(long timeout, TimeUnit unit) on LDClientInterface so apps can flush buffered analytics events and learn whether delivery succeeded before the process or client goes away—unlike fire-and-forget flush().

DirectEventProcessor implements timed blockingFlush: delivery runs on the scheduler via a Callable and returns true when the service accepts the payload (or there is nothing to send), false when offline, stopped, send fails, or the wait times out. On timeout the in-flight HTTP work is not cancelled because the buffer was already drained.

LDClient.flushAndWait applies one timeout budget across all configured environments (remaining time is shared, not per env) and returns false if the client is closed.

EventProcessor gets a default timed blockingFlush so older custom processors still compile (unbounded flush, reports success).

Tests cover success, empty buffer, offline, timeout, closed client, and instrumented LDClient paths. The test app adds Eval+Kill now vs Eval+Crash now and FlushOnCrashHandler to demonstrate rescue via flushAndWait in an uncaught-exception handler versus instant SIGKILL.

Reviewed by Cursor Bugbot for commit 4ac8572. Bugbot is set up for automated code reviews on this repo. Configure here.

abelonogov-ld and others added 29 commits September 9, 2026 11:59
Tier 2 of the event durability spec: recoverable failure.

flushAndWait(timeout) delivers what has been recorded and reports whether it got
there inside the budget, so an application that knows it is about to go away —
backgrounding, or an uncaught exception handler on its way out — gets an answer
instead of a fire-and-forget flush. Delivery now reports its outcome so the
bounded call can tell delivered from not.

Spec: Event Durability, "Tier 2 — recoverable failure", §10.
Co-authored-by: Cursor <cursoragent@cursor.com>
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	example/src/main/java/com/launchdarkly/example/MainActivity.java
#	example/src/main/res/layout/activity_main.xml
…-durability-tier2-bounded-flush

* andrey/event-durability-tier1-buffer:
  unserializable unit test
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java
…-durability-tier2-bounded-flush

* andrey/event-durability-tier1-buffer:
  test(fdv2): stop requiring a changeset to be the first result
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DirectEventProcessor.java
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java
…-durability-tier2-bounded-flush

* andrey/event-durability-tier1-buffer:
  Document that close() discards events held while offline
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java
@abelonogov-ld
abelonogov-ld requested a review from a team as a code owner September 23, 2026 04:02

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit df9e409. Configure here.

flushAndWait started from delivered = true and only updated it for the
environments this client still belongs to. After close() that set is
empty, so the call reported success for events it could not deliver.
It now returns false when the client has been closed or replaced.

Co-authored-by: Cursor <cursoragent@cursor.com>
…-durability-tier2-bounded-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant