feat(events): summarize and buffer events on the recording thread (tier 1) - #397
Open
abelonogov-ld wants to merge 11 commits into
Open
abelonogov-ld wants to merge 11 commits into
abelonogov-ld wants to merge 11 commits into
Conversation
…er 1) Tier 1 of the event durability spec: the buffer. AndroidEventBuffer folds an evaluation into summary counters as it is recorded and holds in full only the events that have to be sent one by one, so a burst of evaluations of untracked flags cannot displace anything. AndroidEventProcessor takes over from java-sdk-internal's DefaultEventProcessor in ComponentsImpl, removing the bounded queue that used to sit between the calling thread and the summarizer. Spec: Event Durability, "Tier 1 — the buffer". Co-authored-by: Cursor <cursoragent@cursor.com>
The next commit drops the split-package hack in favor of declarations that launchdarkly/java-core#214 makes public. That is not in a release yet, so this points the build at a working copy until one carries it. Skipped when the directory is absent, so a clean checkout still resolves the published artifact rather than failing. Revert together with a version bump once the release is out. Co-authored-by: Cursor <cursoragent@cursor.com>
…package AndroidEventBuffer was declared in com.launchdarkly.sdk.internal.events so it could reach the summarizers and the output formatter, which were package-private there. That made a package split across two artifacts, forced the class to be public for the rest of the SDK to use it, and needed a javadoc exclusion plus a classpath workaround to keep it out of the published docs. launchdarkly/java-core#214 makes those declarations public, so the class moves to com.launchdarkly.sdk.android where it belongs. It is package-private now, along with its members and Payload, because everything using it is in that package, so it is out of the published docs by virtue of its visibility rather than by being excluded from javadoc. The build.gradle javadoc block goes back to what it was before tier 1. No behavior change: same summarization, same wire format, same tests. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Diagnostics are meant to be off in both states, and updateScheduledTasks cancels the periodic task accordingly, but two paths could still reach the sender after the state had changed. Cancelling does not stop a run that has already begun, and the init event is submitted to the executor while still online, so going offline or backgrounding between submission and execution left it to send anyway. Both paths now re-check on the way out, via a predicate that mirrors the scheduling condition so the two cannot drift. The check in sendDiagnosticStats sits ahead of createEventAndReset, which clears the counters it returns, so a suspended period defers its statistics rather than discarding them. Analytics delivery is unchanged: deliverPayload already skips while offline, and it deliberately keeps running in the background so events recorded before the app was backgrounded still get out. Co-authored-by: Cursor <cursoragent@cursor.com>
The Android prefix in this package is reserved for adapters over Android OS APIs - AndroidPlatformState, AndroidTaskExecutor, AndroidEnvironmentReporter. Neither of these touches an Android API, so the prefix put them in the wrong category while also repeating what the repository and package already say. AndroidEventProcessor -> DirectEventProcessor AndroidEventBuffer -> OutboundEventBuffer DirectEventProcessor names what sets it apart from java-sdk-internal's DefaultEventProcessor: nothing sits between the caller and the summarizer. OutboundEventBuffer says which direction the events are going, which matters once tier 3 adds a store to read them back from. DefaultEventProcessor was avoided as a name because java-sdk-internal already exports that simple name and this package imports from it. Rename only, including the test class. No behavior change. Co-authored-by: Cursor <cursoragent@cursor.com>
- drop OutboundEventBuffer.isEmpty, which nothing calls
- StandardCharsets.UTF_8 in place of Charset.forName("UTF-8")
- scheduleWithFixedDelay, so a cached process does not come back owing every missed run
- split an over-long Javadoc sentence
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 123cb81)
3 tasks
abelonogov-ld
added a commit
that referenced
this pull request
Sep 18, 2026
**Requirements** - [ ] I have added test coverage for new or changed functionality — workflow-only change - [x] I have followed the repository's pull request submission guidelines - [ ] I have validated my changes against all supported platform versions — CI will validate the configured API level **Related issues** Unblocks [#397](#397). Its CI fails during Android SDK setup. **Describe the solution you've provided** Upgrade `android-actions/setup-android` from v3 to v4. Licenses are already accepted successfully; the failure happens afterward because v3 invokes `sdkmanager tools`, and Google no longer publishes that obsolete package. Version 4 removes `tools` from its default package list while continuing to install command-line/platform tools and accept SDK licenses. **Describe alternatives you've considered** Retrying cannot fix a package removed from Google's repository. Adding a separate license-acceptance step would duplicate behavior and would not address the failing `sdkmanager tools` invocation inside v3. **Additional context** Observed error: `Warning: Failed to find package 'tools'`. The upstream v4 change explicitly fixes this failure. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > **Fixes CI Android SDK setup** by upgrading the shared composite CI action from `android-actions/setup-android@v3` to **v4.0.4** (pinned to commit `be39fa834029ff78f1a44aa3bb0819b8fc2bd8fd`). > > v3 fails after license acceptance because it still runs `sdkmanager tools`, a package Google removed from the repository (`Failed to find package 'tools'`). v4 stops requesting that obsolete package while still installing the command-line/platform tooling needed for `./gradlew` builds and the rest of the workflow. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d6a3b7e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ 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 9569078. Configure here.
Bugbot flagged that diagnosticInitSent is set without consulting the Result. That is deliberate: DefaultEventProcessor does the same, and a failed post returns an unsuccessful Result rather than throwing, so the flag flips either way. Note it at the assignment so the next reader does not read it as an oversight. Co-authored-by: Cursor <cursoragent@cursor.com>
Going offline cancels the periodic flush, so coming back online started a fresh interval and left whatever the outage buffered waiting for it. Each drop re-anchored that interval, so repeated brief losses could defer delivery well past a single one. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Requirements
Related issues
Tier 1 of the Event Durability spec, "Tier 1 — the buffer". Tiers 2 and 3 are stacked on this branch and will follow as separate pull requests.
Describe the solution you've provided
DirectEventProcessortakes over from java-sdk-internal'sDefaultEventProcessorinComponentsImpl, andOutboundEventBufferreplaces the bounded queue that used to sit between the calling thread and the summarizer.That queue is the problem this tier removes. An evaluation handed its event to an
ArrayBlockingQueueand a dispatcher thread summarized it on the far side, so a burst of evaluations of untracked flags could fill the queue and displace thetrackandidentifyevents an application actually asked to send. Summarizing on the recording thread means an evaluation of an untracked flag costs a counter increment and can never displace anything, because it never occupies a slot in the first place.OutboundEventBufferholds in full only the events that have to be sent one by one. It reuses java-sdk-internal'sEventOutputFormatterand summarizer implementations rather than reimplementing them, so there stays exactly one definition of what an event looks like on the wire.Two smaller changes ride along, both prerequisites rather than additions:
Describe alternatives you've considered
Reuse
DefaultEventProcessorand raise its queue capacity. This moves the threshold without removing it. The displacement is a property of having a bounded handoff between the caller and the summarizer at all, and a render loop re-evaluating a tracked flag will reach any capacity you pick.Keep the buffer inside java-sdk-internal's package to reach its package-private types. This worked, but it relied on a split package, which breaks Java modules and needed a Javadoc exclusion to build at all. Making the types public upstream was the durable fix, and is why the dependency bump is here.
Additional context
Naming.
DirectEventProcessorandOutboundEventBufferrather than anAndroidprefix: that prefix in this package is reserved for adapters over Android OS APIs, such asAndroidPlatformStateandAndroidTaskExecutor, and neither of these classes touches one. "Direct" names the contrast withDefaultEventProcessor— nothing sits between the caller and the summarizer.DefaultEventProcessorwas unavailable as a name because java-sdk-internal already exports it and this package imports from it.Tests.
DirectEventProcessorTest,EventProcessorBufferingTestandEventProcessorPrivacyTestshare anEventProcessorTestBase, adding 22 tests across buffering, capacity, private-attribute redaction and diagnostics. The full unit suite is 752 tests, all passing locally.Platform validation. The requirements box above is left unchecked deliberately. Behaviour is covered by the unit suite, but performance was measured on one physical device rather than across the supported API range.
Example app. Gains an "Eval+track+kill" button that evaluates a flag, tracks an event, flushes, and then kills the process five seconds later. That is the reproduction for the loss this work addresses, and it still loses the events on this tier — tier 3 is what makes them survive. Its
LDClient.initcall also moves off the deprecated three-argument overload.Note
Overview
Replaces the Android SDK’s
DefaultEventProcessorwrapper withDirectEventProcessorandOutboundEventBuffer, so flag evaluations are summarized on the recording thread and only full-fidelity events (track, identify, tracked flags, debug) use the capacity-limited buffer. That removes the old bounded inbox queue where evaluation bursts could silently drop unrelated custom events.ComponentsImplnow buildsDirectEventProcessor+OutboundEventBuffer, reusing java-sdk-internal’s wire format vialaunchdarkly-java-sdk-internal1.12.0 (summarizer/formatter types made public). Diagnostics are not sent while offline or backgrounded; coming back online triggers an immediate delivery flush.The example app switches to
LDClient.init(..., INIT_WAIT_SECONDS)and adds an Eval+track+kill button to reproduce in-memory event loss before later durability tiers. New unit tests cover buffering under burst load, close/offline behavior, privacy redaction, and diagnostics.Reviewed by Cursor Bugbot for commit 6d77bde. Bugbot is set up for automated code reviews on this repo. Configure here.