Skip to content

feat: download remaining old lazy splits on new release failure or timeout#250

Open
Yash02Rajput wants to merge 1 commit intomainfrom
download-old-lazy-splits-on-release-failure-or-timeout
Open

feat: download remaining old lazy splits on new release failure or timeout#250
Yash02Rajput wants to merge 1 commit intomainfrom
download-old-lazy-splits-on-release-failure-or-timeout

Conversation

@Yash02Rajput
Copy link
Contributor

@Yash02Rajput Yash02Rajput commented Feb 13, 2026

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved handling of app update download failures and timeout scenarios on both Android and iOS
    • Better state management during interrupted downloads with more graceful error recovery
  • Improvements

    • Enhanced retry mechanism for failed app downloads
    • Better status tracking and notifications during update downloads and recovery processes

@semanticdiff-com
Copy link

Review changes with  SemanticDiff

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Both files enhance failure handling and retry mechanisms for lazy package downloads. The Android UpdateTask now tracks package update state and differentiates between updating the current package versus updating lazy-splits for new releases when failures occur. The iOS ApplicationManager adds a dedicated retry method and integrates it across multiple download paths to gracefully handle failed lazy downloads.

Changes

Cohort / File(s) Summary
Android UpdateTask
airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt
Introduces state tracking (presult, newReleaseFailed) to handle new release lazy-splits flow and fetch failures. Adjusts control flow to conditionally download lazy-splits based on fetched state and presult type, with fallback handling for timeout/failure scenarios.
iOS ApplicationManager
airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m
Adds new public method retryFailedLazyDownloadsWithCompletion: for custom completion handling. Reworks startDownload and tryDownloadingUpdate to delegate immediate lazy retries to this new method, adding completion blocks and integrating retry logic across multiple code paths including timed-lazy-download and release config failure scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 In lazy lands where downloads fail,
Our little bunny won't turn pale,
Retry logic hops and bounds,
Through Android splits and iOS grounds,
Graceful handling, second chance—
Watch those packages prance and dance! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the pull request: implementing fallback behavior to download remaining old lazy splits when a new release fails or times out.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch download-old-lazy-splits-on-release-failure-or-timeout

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt (1)

239-259: ⚠️ Potential issue | 🟠 Major

Logic gap confirmed: lazy splits may be downloaded for incomplete packages when important splits fail after timeout.

When updateTimedOut=true and presult is Update.Package.Failed:

  • newReleaseFailed evaluates to false (not preventing lazy download)
  • shouldDownloadNewLazy evaluates to true (allowing lazy download despite failed important splits)

The code will attempt to download and persist lazy splits for the new package even though the important splits failed. A new TempWriter is created at line 709, and results are persisted via saveDownloadedPackages (line 589–591), but the package remains incomplete and unusable without the important splits.

The proposed fix is sound:

Recommended fix
-        val shouldDownloadNewLazy = fetched != null && !newReleaseFailed &&
-            (presult is Update.Package.Finished || updateTimedOut.get())
+        val shouldDownloadNewLazy = fetched != null && !newReleaseFailed &&
+            presult is Update.Package.Finished

This ensures lazy splits are only downloaded when important splits succeeded, preventing wasted downloads of unusable packages.

However, clarify the design intent: if timeout-triggered partial progress is meant to support future retries with separately-retried important splits, document this explicitly; otherwise, apply the fix to avoid persisting incomplete packages.

🤖 Fix all issues with AI agents
In
`@airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m`:
- Around line 736-743: The code mutates and iterates the NSMutableArray
strongSelf.downloadedLazy from concurrent callbacks in singleDownloadHandler
(called by downloadLazyPackageResources:), causing a data race; fix by
serializing all accesses to downloadedLazy—wrap the loop that reads/mutates
downloadedLazy and any other reads (the earlier read at line ~750) with a single
synchronization mechanism (e.g., a dedicated serial dispatch_queue, NSLock
property, or `@synchronized`(self.downloadedLazy)) and use the same lock/queue
consistently wherever downloadedLazy is read or written (references:
downloadedLazy, singleDownloadHandler, downloadLazyPackageResources:,
AJPLazyResource.isDownloaded). Ensure you acquire the lock before
iterating/modifying and release after the update.
🧹 Nitpick comments (3)
airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m (2)

1341-1387: Eliminate duplication: retryFailedLazyDownloads and retryFailedLazyDownloadsWithCompletion: are nearly identical.

The new method duplicates the body of retryFailedLazyDownloads (lines 1297–1339) almost verbatim. Consider consolidating by having the original method delegate to the new one:

♻️ Proposed refactor
 - (void)retryFailedLazyDownloads {
-    NSMutableArray<AJPLazyResource *> *failedDownloads = [NSMutableArray array];
-    // ... duplicated body ...
+    [self retryFailedLazyDownloadsWithCompletion:nil];
 }

This keeps a single implementation path and reduces maintenance burden.


721-753: Verify: nested weakSelf/strongSelf re-declarations shadow the outer capture.

Lines 727–730 and 745–748 re-declare __strong AJPApplicationManager* strongSelf = weakSelf; inside the inner blocks, shadowing the outer strongSelf from line 717. While this is correct Objective-C (each block creates a new scope), mixing two scopes with the same variable name is easy to mis-read. The weakSelf nil-checks on lines 727 and 745 correctly guard the inner usage, so this is functionally fine but worth noting for readability.

airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt (1)

231-231: Non-null assertion presult!! is safe but unidiomatic — the enclosing if already checks presult != null.

Since presult is a local var, Kotlin's smart cast doesn't apply across the else if boundary. The !! works but a let or a local val would be cleaner.

♻️ Suggested alternative
-            } else if (presult != null && fetched.pkg.lazy.isEmpty()) {
-                saveDownloadedPackages(presult!!, fetched.pkg)
+            } else if (presult != null && fetched.pkg.lazy.isEmpty()) {
+                presult?.let { saveDownloadedPackages(it, fetched.pkg) }
             }

Comment on lines +736 to +743
for (NSUInteger i = 0; i < strongSelf.downloadedLazy.count; i++) {
AJPLazyResource *lazyResource = strongSelf.downloadedLazy[i];
if ([lazyResource.filePath isEqualToString:resource.filePath]) {
lazyResource.isDownloaded = status;
strongSelf.downloadedLazy[i] = lazyResource;
break;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Data race: concurrent mutation of downloadedLazy without synchronization.

singleDownloadHandler is invoked concurrently from multiple GCD blocks dispatched inside downloadLazyPackageResources:. Each callback iterates and mutates strongSelf.downloadedLazy (an NSMutableArray) without any lock or @synchronized block. This can cause crashes or corrupted state.

🔒 Proposed fix — synchronize access to downloadedLazy
+                               `@synchronized`(strongSelf.downloadedLazy) {
                                for (NSUInteger i = 0; i < strongSelf.downloadedLazy.count; i++) {
                                    AJPLazyResource *lazyResource = strongSelf.downloadedLazy[i];
                                    if ([lazyResource.filePath isEqualToString:resource.filePath]) {
                                        lazyResource.isDownloaded = status;
                                        strongSelf.downloadedLazy[i] = lazyResource;
                                        break;
                                    }
                                }
+                               }

Apply the same synchronization at line 750 where strongSelf.downloadedLazy is read.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (NSUInteger i = 0; i < strongSelf.downloadedLazy.count; i++) {
AJPLazyResource *lazyResource = strongSelf.downloadedLazy[i];
if ([lazyResource.filePath isEqualToString:resource.filePath]) {
lazyResource.isDownloaded = status;
strongSelf.downloadedLazy[i] = lazyResource;
break;
}
}
`@synchronized`(strongSelf.downloadedLazy) {
for (NSUInteger i = 0; i < strongSelf.downloadedLazy.count; i++) {
AJPLazyResource *lazyResource = strongSelf.downloadedLazy[i];
if ([lazyResource.filePath isEqualToString:resource.filePath]) {
lazyResource.isDownloaded = status;
strongSelf.downloadedLazy[i] = lazyResource;
break;
}
}
}
🤖 Prompt for AI Agents
In
`@airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m`
around lines 736 - 743, The code mutates and iterates the NSMutableArray
strongSelf.downloadedLazy from concurrent callbacks in singleDownloadHandler
(called by downloadLazyPackageResources:), causing a data race; fix by
serializing all accesses to downloadedLazy—wrap the loop that reads/mutates
downloadedLazy and any other reads (the earlier read at line ~750) with a single
synchronization mechanism (e.g., a dedicated serial dispatch_queue, NSLock
property, or `@synchronized`(self.downloadedLazy)) and use the same lock/queue
consistently wherever downloadedLazy is read or written (references:
downloadedLazy, singleDownloadHandler, downloadLazyPackageResources:,
AJPLazyResource.isDownloaded). Ensure you acquire the lock before
iterating/modifying and release after the update.

@Yash02Rajput Yash02Rajput force-pushed the download-old-lazy-splits-on-release-failure-or-timeout branch 2 times, most recently from d3c0f73 to ffa79dc Compare February 13, 2026 11:55
@Yash02Rajput Yash02Rajput force-pushed the download-old-lazy-splits-on-release-failure-or-timeout branch from ffa79dc to b794bf2 Compare February 17, 2026 06:10
@Yash02Rajput Yash02Rajput force-pushed the download-old-lazy-splits-on-release-failure-or-timeout branch from b794bf2 to 39050f9 Compare February 28, 2026 05:13
@Yash02Rajput Yash02Rajput force-pushed the download-old-lazy-splits-on-release-failure-or-timeout branch from 608373f to 39050f9 Compare March 3, 2026 11:47
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