Skip to content

fix: stop the upload loop when no batch can be prepared - #925

Merged
thomson-t merged 1 commit into
mainfrom
fix/upload-loop-no-progress
Oct 14, 2026
Merged

thomson-t merged 1 commit into
mainfrom
fix/upload-loop-no-progress

Conversation

@thomson-t

Copy link
Copy Markdown
Contributor

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: UploadHandler prepared batches in a while (hasMessagesForUpload()) loop. prepareMessageUploads returned silently when the MParticle instance was gone, and it swallowed every error from the database step, so the loop re-read the same pending messages forever. After: prepareMessageUploads returns 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 main only. 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 Checks and Kotlin Lint Checks jobs on this pull request.

Risks

  • an external caller of prepareMessageUploads breaks on the changed return type; the method is on an internal class that is not listed in scripts/api-frozen-internals.txt, and its only callers are inside android-core; apiCheck passes with the regenerated dump, and the Binary Compatibility job may flag the changed method
  • events behind an unpackageable message stay queued; not addressed here, because dropping data needs its own decision; they would show up as stored events that never upload

Risk 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-core unit tests (313, including the new UploadHandlerTest case), apiCheck, ktlintCheck, lintDebug and scripts/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.prepareMessageUploads goes from returning V to returning Z.

🤖 Generated with Claude Code

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>
@github-actions

Copy link
Copy Markdown

Kotlin migration progress

⚠️ This pull request adds 6 lines of Java to the published modules.

Module Kotlin LOC Java LOC Java staying (facade) Java left to convert Kotlin share
android-core 3,249 22,535 9,650 12,885 12.6%
android-kit-base 557 5,385 1,651 3,734 9.4%
Total 3,806 27,920 11,301 16,619 12.0%

Conversion progress: 0.0% (16,619 of 16,550 baseline Java LOC left to convert). 132 Java files and 36 Kotlin files in scope.

@github-actions

Copy link
Copy Markdown

Binary compatibility

  • ✅ android-core is compatible with 6.1.8 (115 classes compared, 0 with additions only).
  • ✅ android-kit-base is compatible with 6.1.8 (44 classes compared, 0 with additions only).

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 api-compat-reports workflow artifact. This check is not in the required-check set yet, so a red result here does not block merging.

@github-actions

Copy link
Copy Markdown

📦 SDK Size Impact Report

What the SDK adds to a minified release APK.

mParticle Core SDK

Measured 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.

Metric Target branch This PR Change
APK size 119.79 KB 119.81 KB +19 bytes
Download size 117.61 KB 117.63 KB +21 bytes
Dex bytes 213.76 KB 213.77 KB +8 bytes

mParticle Core + Rokt kit

Measured 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 size-report-rokt/README.md.

Metric Target branch This PR Change
APK size 1.26 MB 1.26 MB 0 bytes
Download size 1.24 MB 1.24 MB +13 bytes
Dex bytes 2.22 MB 2.22 MB +8 bytes

Rokt SDK+ umbrella (adds the payment extension)

Metric Target branch This PR Change On top of mParticle Core + Rokt kit
APK size 6.53 MB 6.53 MB 0 bytes +5.27 MB
Download size 6.43 MB 6.43 MB -124 bytes +5.19 MB
Dex bytes 7.11 MB 7.11 MB +20 bytes +4.88 MB

➡️ SDK size impact change is minimal.

Raw measurements

core, 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}

Measured 63bd665 merged into 2b8e6d2

@thomson-t
thomson-t marked this pull request as ready for review October 13, 2026 19:23
@thomson-t
thomson-t requested a review from a team as a code owner October 13, 2026 19:23
@thomson-t
thomson-t merged commit 0cd5868 into main Oct 14, 2026
90 of 91 checks passed
@thomson-t
thomson-t deleted the fix/upload-loop-no-progress branch October 14, 2026 15:16
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.

2 participants