Repository navigation
fix: stop the upload loop when no batch can be prepared - #925
Conversation
The upload loop prepares batches until no pending messages remain. When preparation made no progress (the database step failed or the SDK instance was gone), the same pending messages were read again on every pass and the upload thread never left the loop. prepareMessageUploads now returns whether it packaged the pending messages, and the loop stops on the first pass that did not. Batches already prepared are still uploaded, and the next upload is scheduled as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Kotlin migration progress
Conversion progress: 0.0% (16,619 of 16,550 baseline Java LOC left to convert). 132 Java files and 36 Kotlin files in scope. |
Binary compatibility
Compares the release AAR against the same artifact at the last version published to Maven Central; R8-renamed classes and members are excluded (see the script docstring). Full japicmp HTML/XML reports are attached as the |
📦 SDK Size Impact ReportWhat the SDK adds to a minified release APK. mParticle Core SDKMeasured against an empty baseline app. Unlike the Rokt kit, android-core ships no Compose and no resources, so there is nothing here that a host app would already provide.
mParticle Core + Rokt kitMeasured against a Compose + Material3 reference app, so these are the costs on top of an app that already ships Compose. The reference app's dependencies are a documented convention, not a measured average: see
Rokt SDK+ umbrella (adds the payment extension)
➡️ SDK size impact change is minimal. Raw measurementscore, Target branch: {"baseline_dex_bytes": 0, "baseline_download_bytes": 2509, "baseline_install_bytes": 7525, "core_dex_bytes": 218892, "core_download_bytes": 122941, "core_install_bytes": 130193}core, This PR: {"baseline_dex_bytes": 0, "baseline_download_bytes": 2509, "baseline_install_bytes": 7526, "core_dex_bytes": 218900, "core_download_bytes": 122962, "core_install_bytes": 130213}rokt, Target branch: {"baseline_dex_bytes": 1829108, "baseline_download_bytes": 1240062, "baseline_install_bytes": 1300934, "kit_dex_bytes": 4162180, "kit_download_bytes": 2542337, "kit_install_bytes": 2623275, "sdkplus_dex_bytes": 9282848, "sdkplus_download_bytes": 7980435, "sdkplus_install_bytes": 8148975}rokt, This PR: {"baseline_dex_bytes": 1829108, "baseline_download_bytes": 1240062, "baseline_install_bytes": 1300934, "kit_dex_bytes": 4162188, "kit_download_bytes": 2542350, "kit_install_bytes": 2623275, "sdkplus_dex_bytes": 9282868, "sdkplus_download_bytes": 7980311, "sdkplus_install_bytes": 8148975} |
Why
When the SDK cannot package stored events for upload, for example because one stored event is unreadable, its upload thread can get stuck. It retries the same events in a tight loop and never moves on. Once this lands, the SDK stops after one failed attempt and tries again on the next upload, so the thread keeps running normally.
Programme
Standalone change, no programme.
What changes
Before:
UploadHandlerprepared batches in awhile (hasMessagesForUpload())loop.prepareMessageUploadsreturned silently when theMParticleinstance was gone, and it swallowed every error from the database step, so the loop re-read the same pending messages forever. After:prepareMessageUploadsreturns whether it packaged the messages, and the loop stops on the first pass that made no progress. Batches already prepared are still uploaded, the next upload is scheduled under the same conditions as before, and the low-heap exception still propagates. No documentation or changelog change is needed; the release notes are generated from the commit message.Where to start reading:
UploadHandler.handleMessageImpl. Deliberately left alone: what happens to a stored message that can never be packaged. It now waits for each later upload instead of spinning the thread.Linked work
None.
Rollout
Path: merging changes
mainonly. The fix ships in the next release of the core SDK (android-core), and apps get it when they upgrade. It is live as soon as an app runs that release, with no switch and no ordering constraint.Feature flags: none.
Turning it off: revert this pull request and publish a new release. A published version cannot be recalled, so apps on the release that carries it keep the change until they upgrade again.
What we watch: nothing monitors this path. We watch the issues reported on this repository after the release that carries it, and the
Unit Tests,Lint ChecksandKotlin Lint Checksjobs on this pull request.Risks
prepareMessageUploadsbreaks on the changed return type; the method is on an internal class that is not listed inscripts/api-frozen-internals.txt, and its only callers are insideandroid-core;apiCheckpasses with the regenerated dump, and theBinary Compatibilityjob may flag the changed methodRisk class: low.
Who
Written by: an automated coding agent (Claude Code), working for a maintainer who asked for this fix.
Code reviewed before opening: an independent review agent, 13 October 2026; it found nothing to change.
Design reviewed before opening: no one.
Decision this implements: a maintainer's request on 13 October 2026; there is no public record.
Checked:
android-coreunit tests (313, including the newUploadHandlerTestcase),apiCheck,ktlintCheck,lintDebugandscripts/check_api_dump.py --base origin/main, on 13 October 2026.Not checked: a device run that reproduces the stuck loop, because it needs a corrupted on-device database.
Size
Hand-written: 68 lines changed (added plus removed) in 2 files.
Generated: 1 file.
Notes for reviewers
Generated file:
android-core/api/android-core.api, regenerated with./gradlew apiDump. It changes one line:UploadHandler.prepareMessageUploadsgoes from returningVto returningZ.🤖 Generated with Claude Code