Skip to content

fix: backport #489 - app hang in didEnterBackground by making ConnectionInformationStore writes async (v9) - #492

Merged
tanderson-ld merged 1 commit into
v9from
devin/1774384052-backport-489-to-v9
Mar 26, 2026
Merged

fix: backport #489 - app hang in didEnterBackground by making ConnectionInformationStore writes async (v9)#492
tanderson-ld merged 1 commit into
v9from
devin/1774384052-backport-489-to-v9

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

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.storeConnectionInformation writes from a synchronous UserDefaults.set call to an async dispatch on a dedicated serial DispatchQueue. This prevents the main thread from blocking during background transitions, which was causing 5000+ ms hangs — particularly on MDM-managed devices with concurrent NSUserDefaults access.

Reads via retrieveStoredConnectionInformation go directly to UserDefaults without queue serialization, since UserDefaults reads are thread-safe per Apple docs.

Key points for review:

  • The cherry-pick applied cleanly — the ConnectionInformationStore.swift file on v9 was identical to v11's pre-fix state
  • Writes are now eventually consistent. Connection information is advisory/diagnostic state, so this is acceptable
  • retrieveStoredConnectionInformation is only called once during LDClient.init, when no writes are in-flight, so stale reads are not a practical concern

Describe alternatives you've considered

See discussion on #489 — wrapping reads in storeQueue.sync was 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.storeConnectionInformation persistence to a dedicated serial DispatchQueue instead of writing to UserDefaults synchronously.

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.

…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>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team as a code owner March 24, 2026 20:30
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@tanderson-ld

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@tanderson-ld
tanderson-ld merged commit bf10913 into v9 Mar 26, 2026
8 of 14 checks passed
@tanderson-ld
tanderson-ld deleted the devin/1774384052-backport-489-to-v9 branch March 26, 2026 20:59
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>
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.

3 participants