fix: backport #489 - app hang in didEnterBackground by making ConnectionInformationStore writes async (v9) - #492
Merged
Conversation
…ore writes async (#489) ## Summary - Move `ConnectionInformationStore.storeConnectionInformation` from a synchronous `UserDefaults.set` call to an **async dispatch** on a dedicated serial queue, preventing the main thread from blocking during background transitions - Reads go directly to `UserDefaults` without queue serialization — `UserDefaults` reads are thread-safe and this avoids blocking the main thread if a slow write is in-flight Fixes #488 ## Problem When the app transitions to background, `LDClient.didEnterBackground` synchronously writes connection information to `NSUserDefaults` on the main thread: ``` LDClient.didEnterBackground → Thread.performOnMain (DispatchQueue.main.sync) → runMode.didSet → connectionInformation.didSet → ConnectionInformationStore.storeConnectionInformation → UserDefaults.set → -[NSOperation waitUntilFinished] → BLOCKED (5000+ ms) ``` `NSUserDefaults.set` can trigger cross-process synchronization that blocks the main thread, especially on MDM-managed devices with concurrent `NSUserDefaults` access. This causes a watchdog hang of 5000+ ms. In our production app this has caused **7,016 hang events across 653 users**. ## Change **File:** `LaunchDarkly/LaunchDarkly/ServiceObjects/Cache/ConnectionInformationStore.swift` ```diff final class ConnectionInformationStore { private static let connectionInformationKey = "..." + private static let storeQueue = DispatchQueue(label: "com.launchDarkly.ConnectionInformationStore.storeQueue") static func retrieveStoredConnectionInformation() -> ConnectionInformation? { - UserDefaults.standard.retrieve(...) + UserDefaults.standard.retrieve(...) // no queue — UserDefaults reads are thread-safe } static func storeConnectionInformation(connectionInformation: ConnectionInformation) { - UserDefaults.standard.save(...) + storeQueue.async { + UserDefaults.standard.save(...) + } } } ``` ## Design decisions - **Writes are async**: The `UserDefaults.set` call that causes the 5s+ hang is dispatched to a background serial queue, unblocking the main thread - **Reads bypass the queue**: `UserDefaults` reads are inherently thread-safe (Apple documentation). `retrieveStoredConnectionInformation` is only called once during `LDClient.init`, when no writes are in-flight. Wrapping reads in `storeQueue.sync` would reintroduce the main-thread blocking if a slow write were queued - **Connection information is advisory state** (diagnostic/logging) — eventual consistency is acceptable ## Test plan - All 596 existing tests pass with 0 failures - Verified `swift build` compiles cleanly - Verified `swift test` passes all test suites <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes persistence semantics by making `ConnectionInformationStore` writes asynchronous, which could introduce timing/race issues (e.g., last-write not yet flushed) though scope is limited to cached connection diagnostics. > > **Overview** > Prevents main-thread stalls when persisting connection diagnostics by dispatching `ConnectionInformationStore.storeConnectionInformation` UserDefaults writes onto a dedicated serial `DispatchQueue`. > > Keeps `retrieveStoredConnectionInformation` reading directly from `UserDefaults` (no queue serialization) and simplifies key access to use the local `connectionInformationKey` constant. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c2e2abf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
tanderson-ld
approved these changes
Mar 24, 2026
Contributor
|
bugbot run |
abelonogov-ld
approved these changes
Mar 25, 2026
abelonogov-ld
approved these changes
Mar 25, 2026
6 tasks
tanderson-ld
added a commit
that referenced
this pull request
Mar 27, 2026
…r simulators (#495) **Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/v9/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** Unblocks v9 releases — the `macos-13` runner is no longer supported by GitHub Actions, causing all workflows to fail with: > The configuration 'macos-13-us-default' is not supported **Describe the solution you've provided** Aligns v9 GitHub Actions workflows, CI composite action, build scripts, and tooling with v10's configuration: **Workflow files** (`ci.yml`, `release-please.yml`, `manual-publish.yml`, `manual-publish-docs.yml`): - `macos-13` → `macos-15` (primary) / `macos-14` (secondary, in `ci.yml` matrix) - Xcode `15.0.1` / `14.3.1` → `16.4.0` / `15.4.0` - iOS simulators updated to `iPhone 16` / `iPhone 15` (OS version pins removed, matching v10) **CI composite action** (`.github/actions/ci/action.yml`): - Added explicit `brew install swiftlint` step (no longer pre-installed on newer runners) - Added explicit `gem install xcpretty` step - Added SwiftLint and Sourcery failure output logging steps (for debugging build failures) - Renamed swiftlint step for clarity **Xcode project build scripts** (`LaunchDarkly.xcodeproj/project.pbxproj`): - Updated SwiftLint build phases to try system `swiftlint` first, fall back to `mint run`, and log output (matching v10) - Updated Sourcery build phase with logging and error handling (matching v10) **Mintfile**: - SwiftLint `0.43.1` → `0.63.0` (matching v10) - Sourcery `1.2.1` → `2.3.0` (matching v10) - The old versions cannot compile from source on Xcode 16.4 / newer Swift toolchains **`.swiftlint.yml`** _(v9-specific, not on v10)_: - Raised `type_body_length` error threshold from 500 → 550 (v9's `LDClient.swift` is 539 lines; v10 refactored this below 500) - Added `large_tuple` rule config with error threshold of 5 (v9's `DarklyService.swift` has a 4-member tuple that v10 removed) - These are the minimum config changes needed to make v9's existing code pass with v10's SwiftLint version, without modifying SDK source code **Describe alternatives you've considered** Could have pinned to `macos-14` only as a minimal fix, but matching v10 ensures consistency across version branches and avoids needing another migration soon. **Additional context** >⚠️ **Cumulative diff note:** The cumulative diff includes a `ConnectionInformationStore.swift` change — this is from the previously merged backport PR #492 and is *not* part of this PR's changes. Only the `.github/`, `Mintfile`, `.swiftlint.yml`, and `project.pbxproj` files are new here. **CI status:** - `macos-build (15.4.0, macos-14)` — ✅ passing - `macos-build (16.4.0, macos-15)` — ❌ failed due to a **flaky test** (`publishEventData__failure__calls_completion_with_error_and_no_data_or_response` timed out). This is a pre-existing test timing issue, not related to the workflow changes. **Human review checklist:** - [ ] Verify `.swiftlint.yml` threshold changes are acceptable for v9 (these deviate from v10's config since v9's source code differs) - [ ] Confirm Sourcery 2.3.0 code generation is compatible with v9's templates (templates are identical between v9 and v10) - [ ] Consider re-running the macos-15 job to confirm the test failure is flaky and not a real regression Link to Devin session: https://app.devin.ai/sessions/d37a5a4777fb46abb76d0edacf1e10e0 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: tanderson@launchdarkly.com <tanderson@launchdarkly.com>
tanderson-ld
pushed a commit
that referenced
this pull request
Mar 27, 2026
🤖 I have created a release *beep* *boop* --- ## [9.15.1](9.15.0...9.15.1) (2026-03-27) ### Bug Fixes * backport [#489](#489) - app hang in didEnterBackground by making ConnectionInformationStore writes async (v9) ([#492](#492)) ([bf10913](bf10913)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Mostly a Release Please version bump across build metadata and docs; risk is low because there are no functional code changes in this diff beyond version strings/changelog entry. > > **Overview** > Cuts the `9.15.1` release by bumping the SDK version from `9.15.0` to `9.15.1` across the manifest, CocoaPods spec, Xcode project marketing/dylib versions, and `ReportingConsts.sdkVersion`. > > Updates `CHANGELOG.md` with the `9.15.1` entry noting a backported fix for an app hang in `didEnterBackground`, and refreshes the SwiftPM install snippet in `README.md` to reference `9.15.1`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4cf47b9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: LaunchDarklyReleaseBot <LaunchDarklyReleaseBot@launchdarkly.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
Backport of #489 to the v9 branch. Fixes #488.
Describe the solution you've provided
Cherry-pick of the squash merge commit from #489 (merged to v11) onto v9.
The change moves
ConnectionInformationStore.storeConnectionInformationwrites from a synchronousUserDefaults.setcall to an async dispatch on a dedicated serialDispatchQueue. This prevents the main thread from blocking during background transitions, which was causing 5000+ ms hangs — particularly on MDM-managed devices with concurrentNSUserDefaultsaccess.Reads via
retrieveStoredConnectionInformationgo directly toUserDefaultswithout queue serialization, sinceUserDefaultsreads are thread-safe per Apple docs.Key points for review:
ConnectionInformationStore.swiftfile on v9 was identical to v11's pre-fix stateretrieveStoredConnectionInformationis only called once duringLDClient.init, when no writes are in-flight, so stale reads are not a practical concernDescribe alternatives you've considered
See discussion on #489 — wrapping reads in
storeQueue.syncwas considered but rejected because it would reintroduce main-thread blocking if a slow write is queued.Additional context
This is a direct cherry-pick with no modifications. The identical change has already been reviewed, approved, and merged on v11.
Link to Devin session: https://app.devin.ai/sessions/d37a5a4777fb46abb76d0edacf1e10e0
Note
Medium Risk
Introduces asynchronous persistence for connection diagnostics, which can change timing/consistency of stored values and potentially affect any code that expects writes to be immediately visible, but it is limited in scope and not security-critical.
Overview
Prevents background-transition hangs by moving
ConnectionInformationStore.storeConnectionInformationpersistence to a dedicated serialDispatchQueueinstead of writing toUserDefaultssynchronously.Read behavior is unchanged (still reads directly from
UserDefaults), while the write path becomes eventually consistent.Written by Cursor Bugbot for commit 9b6957d. This will update automatically on new commits. Configure here.