Skip to content

Fix stats widget InterruptedException family (Sentry JETPACK-ANDROID-1ATH and others)#22857

Merged
nbradbury merged 8 commits into
trunkfrom
fix/sentry-widget-interruptedexception
May 14, 2026
Merged

Fix stats widget InterruptedException family (Sentry JETPACK-ANDROID-1ATH and others)#22857
nbradbury merged 8 commits into
trunkfrom
fix/sentry-widget-interruptedexception

Conversation

@nbradbury
Copy link
Copy Markdown
Contributor

@nbradbury nbradbury commented May 12, 2026

Fixes the InterruptedException family across all stats widgets:

Summary

RemoteViewsService.onDataSetChanged() in each stats widget VM uses runBlocking { … fetchXxx() } to refresh data on the widget host thread. When the host kills the service mid-fetch (the user collapses the widget, the device goes to doze, etc.) the coroutine is cancelled and the resulting InterruptedException propagates uncaught, crashing the app.

Combined Sentry user count across all seven IDs above: ~157 distinct users in the last 30 days (Jetpack). Play Console reports the same shape at ~125 users. All seven VMs share the same bug class, so the fix is structurally a single change — bundling them into one PR.

Changes

  • New widget/utils/WidgetCoroutineHelper.kt with runBlockingForWidget(block) — wraps runBlocking and swallows InterruptedException / CancellationException, logging via AppLog.w(T.STATS, …). The VM falls through to its cached getXxx(site) read, so the widget still renders whatever data is already on disk.
  • Each of the seven widget VMs now imports and uses runBlockingForWidget in place of runBlocking around the fetch call. No other behavior changes.

Test plan

There's no easy way to test this so I think a simple code review and clear CI should suffice.

The stats `RemoteViewsService.onDataSetChanged()` path uses `runBlocking`
to fetch fresh data on the widget host thread. When the host kills the
service mid-fetch the coroutine is cancelled and the resulting
`InterruptedException` propagates uncaught, crashing the app.

Add a shared `runBlockingForWidget(block)` helper in `widget/utils/`
that wraps `runBlocking` and swallows `InterruptedException` /
`CancellationException` so the VM can still fall through to its cached
data read. Update every widget VM to use the helper.

VMs affected:
- ViewsWidgetListViewModel        (Sentry JETPACK-ANDROID-1ATH)
- TodayWidgetBlockListViewModel   (Sentry JETPACK-ANDROID-1AV2)
- WeekWidgetBlockListViewModel    (Sentry JETPACK-ANDROID-1AZW)
- TodayWidgetListViewModel        (Sentry JETPACK-ANDROID-1AZQ)
- AllTimeWidgetBlockListViewModel (Sentry JETPACK-ANDROID-1AWZ)
- AllTimeWidgetListViewModel      (Sentry JETPACK-ANDROID-1B0V)
- WeekViewsWidgetListViewModel    (Sentry JETPACK-ANDROID-1C2Y)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented May 12, 2026

1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented May 12, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22857-7c7bbee
Build Number1488
Application IDcom.jetpack.android.prealpha
Commit7c7bbee
Installation URL78t0hmjqggea0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented May 12, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22857-7c7bbee
Build Number1488
Application IDorg.wordpress.android.prealpha
Commit7c7bbee
Installation URL70uba06hui200
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.22%. Comparing base (e8f425b) to head (6b6fca6).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
...efresh/lists/widget/utils/WidgetCoroutineHelper.kt 20.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22857      +/-   ##
==========================================
- Coverage   37.22%   37.22%   -0.01%     
==========================================
  Files        2317     2318       +1     
  Lines      124556   124561       +5     
  Branches    16917    16917              
==========================================
+ Hits        46370    46371       +1     
- Misses      74436    74440       +4     
  Partials     3750     3750              

☔ 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.

nbradbury and others added 4 commits May 13, 2026 07:25
Restore the thread interrupt flag after swallowing InterruptedException
in runBlockingForWidget, per JVM convention. Remove
@Suppress("TooGenericExceptionCaught") — the rule doesn't fire on these
specific exception types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nbradbury nbradbury marked this pull request as ready for review May 13, 2026 12:47
@nbradbury nbradbury requested a review from adalpari May 13, 2026 12:47
} catch (_: InterruptedException) {
AppLog.w(AppLog.T.STATS, "Widget data fetch interrupted")
Thread.currentThread().interrupt()
} catch (_: CancellationException) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

mmm I'm not sure that swallowing CancellationException is a good idea because then the cancel propagation is stopped and upper jobs keep doing their thing.
We should limit calling this function to very specific scenarios where we are sure it has no side effects. (Which I think is the case, but still I'm concerned)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure that swallowing CancellationException is a good idea because then the cancel propagation is stopped and upper jobs keep doing their thing.

We could get rid of this and just catch the InterruptedException, since that's the main source of the crashes. WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering what would happen then when throwing the thread interrupt. But I guess it could work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I asked for Claude's input on this discussion:

Good instinct to flag this — but I think it's safe in this specific spot, and worth me spelling out why in the KDoc so the next reader doesn't have to re-derive it.

The "swallowing CancellationException stops propagation and upper jobs keep running" failure mode applies when there's a parent Job above. Here, runBlocking creates a brand-new root coroutine on the RemoteViewsService worker thread — there's nothing upstream to notify. The CancellationException we see is what runBlocking re-throws when the worker thread is interrupted mid-fetch by the widget host; it's terminal for the local scope, not a signal we're hiding from a surviving caller.

The real risk is someone reusing this helper from inside an existing coroutine, where the concern would apply. I'll tighten the KDoc to call that out explicitly ("only safe because this is a root runBlocking — do not call from inside another coroutine").

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds reasonable.

nbradbury and others added 2 commits May 14, 2026 09:39
… cancel

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@adalpari adalpari left a comment

Choose a reason for hiding this comment

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

After some clarifications, LGTM!

@nbradbury nbradbury enabled auto-merge (squash) May 14, 2026 16:09
@nbradbury nbradbury merged commit ba21502 into trunk May 14, 2026
21 of 22 checks passed
@nbradbury nbradbury deleted the fix/sentry-widget-interruptedexception branch May 14, 2026 16:24
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.

4 participants