fix(offline): keep OfflineManager alive until its async tasks complete#4252
Open
OxMarco wants to merge 1 commit into
Open
fix(offline): keep OfflineManager alive until its async tasks complete#4252OxMarco wants to merge 1 commit into
OxMarco wants to merge 1 commit into
Conversation
`startLoading` creates a function-local `OfflineManager`, uses it to start `loadStylePack` and to vend a tileset descriptor, then returns. Nothing retains it: the progress and completion closures do not capture it, and `cancelables` holds the tasks rather than the manager. The manager is therefore deinitialized as soon as `startLoading` returns, while both asynchronous operations are still running and the Mapbox SDK still holds state tied to it. That aborts with the Swift runtime error "object deallocated with non-zero retain count" in a closure in `startLoading`. The existing DispatchGroup already marks the point at which both tasks are finished, so capture the manager in its `notify` block and hold it until then.
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.
Summary
RNMBXOfflineModule.startLoadingcreates a function-localOfflineManager, kicks off two asynchronous operations with it, and then returns, dropping the only strong reference to it. Nothing keeps the manager alive for the lifetime of the work it started, so Swift can deinitialize it while the Mapbox SDK still holds internal state tied to it.This shows up as a
SIGABRTwith the Swift runtime message "object deallocated with non-zero retain count", in a closure instartLoading.The ownership problem
threadedOfflineManageris a localleton line 358. It is used to startloadStylePack(line 360) and to vend the tileset descriptor (line 383). Neither the closures norcancelables(line 432, which stores the tasks, not the manager) capture it, so its lifetime ends whenstartLoadingreturns, while both async operations are still in flight.OfflineManageris not a fire-and-forget value: it owns native state that the in-flight style pack load and the descriptor it vended are tied to. Releasing it mid-flight lets Swift deinitialize it while the SDK still holds references into it, which is exactly what the abort reports.The fix
The existing
DispatchGroupalready marks the point at which both tasks have completed, so capture the manager in itsnotifyblock and hold it until then.An alternative you may prefer
There is already a
lazy var offlineManager: OfflineManagerproperty on the module (line 33), and it is not obvious whystartLoadingcreates a second, separate instance rather than reusing it. If the per-call instance is not deliberate, holding a single manager as a property would be cleaner than extending the local's lifetime.The name
threadedOfflineManagerhints there may be a threading reason for the separate instance, which is why I went with the minimal lifetime fix rather than changing the ownership model. Happy to switch to whichever you prefer.Testing
yarn generateis a no-op for this change: it touches no components, style properties, or codepart-generated regions.Honest caveat
This is not reproduced deterministically. It is a race, and it fired once across our user base in production. The component below exercises the code path, but whether it aborts depends on the timing of
deinitagainst the SDK's cleanup, and it is most likely when the download completes very quickly or fails early.The ownership bug itself is established by reading the source rather than by a repro: the local manager is provably unretained past the end of
startLoading, while the work it started is provably still running.Environment
@rnmapbox/maps10.3.2