Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 22, 2025

Summary

The reconnection logic had a critical bug where failed reconnection attempts would not schedule subsequent retries, causing local push to permanently fail until app restart. Additionally, rapid status updates could schedule duplicate timers with incorrect backoff timing.

Root cause: scheduleReconnection() was only called when a server was first added to disconnectedServers. After the first reconnection attempt failed, subsequent status checks found the server already tracked and did nothing, breaking the reconnection chain.

Changes:

  • Renamed scheduleReconnection()scheduleReconnectionIfNeeded() with idempotency guards
  • Always call scheduleReconnectionIfNeeded() for unavailable states (not just initial disconnection)
  • Guard against duplicate timers: skip scheduling if timer already active
  • Clear timer reference when fired to enable next attempt scheduling

Flow before fix:

case .unavailable:
    if !disconnectedServers.contains(server.identifier) {
        disconnectedServers.insert(server.identifier)
        scheduleReconnection()  // Only called once
    } else {
        // Subsequent checks do nothing - reconnection stops!
    }

Flow after fix:

case .unavailable:
    if !disconnectedServers.contains(server.identifier) {
        disconnectedServers.insert(server.identifier)
    }
    // Always attempt scheduling - idempotent guards prevent duplicates
    scheduleReconnectionIfNeeded()

Screenshots

N/A - Internal networking logic change

Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#

Any other notes

This PR merges into #4113 and addresses feedback from review comment #4113 (comment).


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits December 22, 2025 11:33
…ous reconnection

The reconnection logic had a critical bug where if a reconnection attempt failed,
no further reconnection attempts would be scheduled. This was because the code only
scheduled a reconnection when a server was first added to the disconnectedServers set,
not when subsequent status checks found it still unavailable.

Additionally, multiple rapid status checks could potentially schedule multiple timers
without proper coordination.

Changes:
- Renamed scheduleReconnection() to scheduleReconnectionIfNeeded() to better reflect its idempotent behavior
- Made scheduleReconnectionIfNeeded() idempotent by checking if a timer is already active before scheduling
- Moved scheduleReconnectionIfNeeded() call outside the if/else block so it's called for ALL unavailable states
- Clear the timer reference in attemptReconnection() after the timer fires
- Updated comments to clarify the behavior

This ensures:
1. Only one timer is active at a time (prevents rapid successive attempts)
2. Failed reconnection attempts properly schedule the next attempt with backoff
3. Proper backoff timing is maintained (timer won't be reset prematurely)

Co-authored-by: bgoncal <[email protected]>
Added more detailed comments to explain:
- Why scheduleReconnectionIfNeeded() is called for all unavailable states
- The idempotent nature of the scheduling method
- Why the timer is cleared before incrementing the attempt counter

Co-authored-by: bgoncal <[email protected]>
Copilot AI changed the title [WIP] Fix local push reliability and reconnection improvements Fix local push reconnection logic to prevent stalled reconnections Dec 22, 2025
Copilot AI requested a review from bgoncal December 22, 2025 11:36
@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

⚠️ Unused L10n strings detected

Found 1 unused localization strings in the codebase.

Click to see details
Parsing Strings.swift...
Found 1274 L10n strings

Reading all Swift source code...
Read 4746813 characters of Swift code

Checking for unused strings...
Checked 100/1274 strings...
Checked 200/1274 strings...
Checked 300/1274 strings...
Checked 400/1274 strings...
Checked 500/1274 strings...
Checked 600/1274 strings...
Checked 700/1274 strings...
Checked 800/1274 strings...
Checked 900/1274 strings...
Checked 1000/1274 strings...
Checked 1100/1274 strings...
Checked 1200/1274 strings...

================================================================================
UNUSED STRINGS REPORT
================================================================================

Found 1 unused strings:


WATCH:
  - L10n.Watch.Labels.noAction
    Key: watch.labels.no_action
    Line: 3913

================================================================================
Total unused: 1
================================================================================

To clean up these strings, manually remove them from the Localizable.strings files
and regenerate Strings.swift using SwiftGen.

@codecov
Copy link

codecov bot commented Jan 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (local-push@d8b556a). Learn more about missing BASE report.

Additional details and impacted files
@@              Coverage Diff              @@
##             local-push    #4134   +/-   ##
=============================================
  Coverage              ?   45.44%           
=============================================
  Files                 ?      242           
  Lines                 ?    13736           
  Branches              ?        0           
=============================================
  Hits                  ?     6242           
  Misses                ?     7494           
  Partials              ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bgoncal bgoncal marked this pull request as ready for review January 6, 2026 16:44
Copilot AI review requested due to automatic review settings January 6, 2026 16:44
@bgoncal bgoncal merged commit e8e40e9 into local-push Jan 6, 2026
11 checks passed
@bgoncal bgoncal deleted the copilot/sub-pr-4113-another-one branch January 6, 2026 16:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical bug in the local push notification reconnection logic where failed reconnection attempts would not schedule subsequent retries, causing permanent connection failures until app restart. The fix ensures that reconnection attempts continue with proper backoff timing by making the scheduling method idempotent and always calling it for unavailable states.

Key Changes:

  • Renamed scheduleReconnection() to scheduleReconnectionIfNeeded() with idempotency guards to prevent duplicate timers
  • Changed unavailable state handling to always call scheduleReconnectionIfNeeded(), not just on initial disconnection
  • Added proper timer lifecycle management by clearing the timer reference when fired, enabling subsequent scheduling

// If still unavailable after this attempt, schedule the next one
// This will be triggered by the state didSet when the connection fails
// If still unavailable after this attempt, the status observer will trigger
// and scheduleReconnectionIfNeeded() will schedule the next attempt with proper backoff
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The comment at line 237 exceeds the 120-character line width limit specified in the SwiftFormat configuration. This comment line should be wrapped to stay within the 120-character maximum line width.

Suggested change
// and scheduleReconnectionIfNeeded() will schedule the next attempt with proper backoff
// and scheduleReconnectionIfNeeded() will schedule the next attempt
// with proper backoff

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
let wasDisconnected = disconnectedServers.contains(server.identifier)
if !wasDisconnected {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The variable wasDisconnected is assigned but never used. It was created to replace the inline check in the if statement, but since it's only used once and the variable name doesn't add clarity beyond the original expression, consider removing it and keeping the original inline check !disconnectedServers.contains(server.identifier).

Suggested change
let wasDisconnected = disconnectedServers.contains(server.identifier)
if !wasDisconnected {
if !disconnectedServers.contains(server.identifier) {

Copilot uses AI. Check for mistakes.
// 1. No timer is already active (prevents duplicate timers)
// 2. There are disconnected servers needing reconnection
// This ensures both initial disconnections and failed reconnection attempts
// properly schedule the next attempt with appropriate backoff.
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The comments at lines 67-72 exceed the 120-character line width limit specified in the SwiftFormat configuration. These comment lines should be wrapped to stay within the 120-character maximum line width.

Suggested change
// properly schedule the next attempt with appropriate backoff.
// properly schedule the next attempt with appropriate backoff and timing.

Copilot uses AI. Check for mistakes.

/// Schedules a reconnection attempt with gradual backoff
private func scheduleReconnection() {
/// Schedules a reconnection attempt with gradual backoff if one is not already scheduled.
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The comment at line 169 exceeds the 120-character line width limit specified in the SwiftFormat configuration. This comment line should be wrapped to stay within the 120-character maximum line width.

Suggested change
/// Schedules a reconnection attempt with gradual backoff if one is not already scheduled.
/// Schedules a reconnection attempt with gradual backoff.
/// If a reconnection is already scheduled, this method does nothing.

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +172
/// This method is idempotent and safe to call multiple times - it will only schedule a new
/// timer if one is not already active. This prevents rapid successive reconnection attempts
/// and ensures proper backoff timing.
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The comments at lines 170-172 exceed the 120-character line width limit specified in the SwiftFormat configuration. These comment lines should be wrapped to stay within the 120-character maximum line width.

Suggested change
/// This method is idempotent and safe to call multiple times - it will only schedule a new
/// timer if one is not already active. This prevents rapid successive reconnection attempts
/// and ensures proper backoff timing.
/// This method is idempotent and safe to call multiple times - it will only schedule a new timer
/// if one is not already active.
/// This prevents rapid successive reconnection attempts and ensures proper backoff timing.

Copilot uses AI. Check for mistakes.
Current.Log
.verbose(
"scheduleReconnection called. Current attempt: \(reconnectionAttempt), timer active: \(reconnectionTimer != nil)"
"scheduleReconnectionIfNeeded called. Current attempt: \(reconnectionAttempt), timer active: \(reconnectionTimer != nil), disconnected servers: \(disconnectedServers.count)"
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The log message at line 176 exceeds the 120-character line width limit specified in the SwiftFormat configuration. This line should be wrapped to stay within the 120-character maximum line width.

Suggested change
"scheduleReconnectionIfNeeded called. Current attempt: \(reconnectionAttempt), timer active: \(reconnectionTimer != nil), disconnected servers: \(disconnectedServers.count)"
"scheduleReconnectionIfNeeded called. "
+ "Current attempt: \(reconnectionAttempt), "
+ "timer active: \(reconnectionTimer != nil), "
+ "disconnected servers: \(disconnectedServers.count)"

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants