Skip to content

Fix hung Google token refresh permanently freezing calendar sync - #950

Open
GeoffreyPlitt wants to merge 3 commits into
leits:masterfrom
GeoffreyPlitt:fix/google-token-refresh-timeout
Open

Fix hung Google token refresh permanently freezing calendar sync#950
GeoffreyPlitt wants to merge 3 commits into
leits:masterfrom
GeoffreyPlitt:fix/google-token-refresh-timeout

Conversation

@GeoffreyPlitt

@GeoffreyPlitt GeoffreyPlitt commented Jul 10, 2026

Copy link
Copy Markdown

Status

READY

Description

Fixes a reliability bug in the Google Calendar provider where a single hung OAuth token refresh permanently freezes all calendar refreshes with no error, no retry, and no user-visible signal.

This caused a real user-facing failure (observed on v4.11.6; the same pattern exists on master): token refreshes stopped silently after a sleep/wake cycle, the menu kept showing the previous day's events all greyed out (past-event styling), the status bar lost its countdown, and no notifications fired until the app was manually restarted.

Root cause (GCEventStore.validAccessToken): token refreshes are serialized through a stored refreshTask, and the task body wraps AppAuth's OIDAuthState.performAction callback in a continuation. If performAction never invokes its callback (observed in the wild — e.g. its internal token-endpoint request gets parked across sleep/wake), the continuation never resumes, defer { refreshTask = nil } never runs, and every subsequent fetch awaits the dead task forever. There was no timeout anywhere in this path. Additionally, the guard let self else { return } inside the callback could leak the continuation outright, and the same serialize-through-a-stored-task pattern in ensureSignedIn/signInTask had the same wedge risk.

Fix:

  • New TimeoutGuardedCompletion helper (in GoogleCalendarPolicy.swift, part of the MeetingBarLogic package) that races a completion-callback-based operation against a hard timeout and guarantees the continuation resumes exactly once, even when the timeout and a late callback race.
  • validAccessToken wraps performAction with a 30s timeout; on timeout it throws AuthError.refreshFailed, refreshTask is always cleared, and the existing ProviderHealth machinery surfaces the stale-data warning as it already does for thrown errors.
  • ensureSignedIn gets the same protection with a 5-minute timeout (interactive sign-in can legitimately take a while), so a never-completing sign-in can no longer block all future refreshes.
  • The guard let self continuation leak is gone — the AppAuth callback no longer touches self.

No public API changes; EventKit provider untouched.

Checklist

  • Localized (no user-facing strings added)
  • Added to changelog — no unreleased section exists yet; happy to add entries once the target version is known:

Steps to Test or Reproduce

  1. make test — new unit tests in MeetingBarLogicTests/TimeoutGuardedCompletionTests.swift cover: a callback that never fires throws within the timeout instead of hanging; a late callback after timeout is safely ignored (no double-resume crash); a subsequent call is not blocked by an earlier hung operation; fast success/failure paths are unaffected.
  2. Full CI (SwiftPM logic tests + app-hosted tests + lint) is green on the fork: 213 + 429 tests, 0 failures.
  3. The hung-callback condition itself only reproduces on rare network/sleep-wake edge cases, so end-to-end verification was regression-focused: sign-in and calendar refresh flow through the new timeout wrapper unchanged.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Google Calendar sign-in now completes with a clear cancellation or timeout result instead of remaining indefinitely.
    • Token refresh operations are time-limited and report a refresh failure when they exceed the allowed duration.
    • Authentication state continues to be cleared appropriately when authorization is no longer valid.
  • Tests
    • Added coverage for sign-in and token-refresh timeout behavior, delayed callbacks, failures, and subsequent operations.

…ever

A hung OIDAuthState.performAction callback (observed parked across sleep/wake in
v4.11.6) never resumed its continuation, so refreshTask stayed set and every
subsequent fetch awaited it indefinitely with no error, no retry, and no
user-visible signal — the menu showed stale, greyed-out events until restart.

Add TimeoutGuardedCompletion, a small helper that races a completion-callback
operation against a hard timeout and guarantees single resumption regardless of
which side wins. Wire it into validAccessToken (30s timeout) and ensureSignedIn
(5min timeout, since interactive sign-in can legitimately take a while), so a
stuck refresh or sign-in now throws instead of hanging, refreshTask/signInTask
are always cleared, and the existing ProviderHealth machinery correctly marks
data stale. Also drops the guard-let-self path inside the performAction
callback that could silently leak the continuation if self were ever nil.
The app target (built without -strict-concurrency=complete, unlike the
MeetingBarLogic SwiftPM package where this compiled fine) couldn't infer
TimeoutGuardedCompletion's T through the nested Task/do-catch in
ensureSignedIn's operation closure. CI caught this at build time.
The app target builds under Swift 6 language mode with strict concurrency
complete. GCEventStore is @mainactor, so the operation closures built at
its two call sites are MainActor-isolated; passing them into the
nonisolated TimeoutGuardedCompletion.run tripped "sending main
actor-isolated value ... risks causing data races". Marking the parameter
`sending` tells the compiler the closure's isolation region transfers
safely into run(), which only ever invokes it synchronously and once.
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Google authentication now bounds interactive sign-in and token refresh operations with timeout guards. A single-resume helper handles late callbacks safely, and tests cover timeout, success, failure, late completion, and independent subsequent calls.

Changes

Google authentication timeout handling

Layer / File(s) Summary
Guarded completion infrastructure
MeetingBar/Calendar/Providers/Google/GoogleCalendarPolicy.swift
Adds timeout errors, callback-versus-timeout racing, and locking to prevent continuation double-resumption.
Google sign-in and refresh integration
MeetingBar/Calendar/Providers/Google/GoogleCalendarEventStore.swift
Applies separate sign-in and token-refresh timeouts while preserving authentication error handling.
Timeout behavior validation
MeetingBarLogicTests/TimeoutGuardedCompletionTests.swift
Tests timeout, fast success, error propagation, late callbacks, and independent later calls.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GCEventStore
  participant TimeoutGuardedCompletion
  participant AppAuth
  GCEventStore->>TimeoutGuardedCompletion: guard sign-in or token refresh
  TimeoutGuardedCompletion->>AppAuth: start completion-based operation
  AppAuth-->>TimeoutGuardedCompletion: return callback result or timeout
  TimeoutGuardedCompletion-->>GCEventStore: return authentication result
Loading

Possibly related PRs

  • leits/MeetingBar#835: Both changes modify Google GCEventStore sign-in handling and signInTask behavior.

Poem

A bunny guards the sign-in gate,
No callback may make us wait.
Refresh hops safely through the night,
Late replies vanish out of sight.
Tests bloom bright in carrot rows—
Auth now bounds its endless woes!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: preventing a hung Google token refresh from blocking calendar sync.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@GeoffreyPlitt
GeoffreyPlitt force-pushed the fix/google-token-refresh-timeout branch from 063decb to 04f1a19 Compare July 10, 2026 21:31

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@MeetingBar/Calendar/Providers/Google/GoogleCalendarEventStore.swift`:
- Around line 298-313: The sign-in timeout in ensureSignedIn should be converted
to AuthError.refreshFailed, matching validAccessToken. Update the surrounding
Task/TimeoutGuardedCompletion error handling to catch OperationTimedOut and
rethrow AuthError.refreshFailed, while preserving other errors unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d509964a-1e24-43e9-b24c-cb18122103c5

📥 Commits

Reviewing files that changed from the base of the PR and between 88519a4 and 04f1a19.

📒 Files selected for processing (3)
  • MeetingBar/Calendar/Providers/Google/GoogleCalendarEventStore.swift
  • MeetingBar/Calendar/Providers/Google/GoogleCalendarPolicy.swift
  • MeetingBarLogicTests/TimeoutGuardedCompletionTests.swift

Comment thread MeetingBar/Calendar/Providers/Google/GoogleCalendarEventStore.swift
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant