Skip to content

Fix sync worker stall on persistent errors#1305

Open
Left024 wants to merge 3 commits into
ReadYouApp:mainfrom
Left024:fix/sync-retry-stall
Open

Fix sync worker stall on persistent errors#1305
Left024 wants to merge 3 commits into
ReadYouApp:mainfrom
Left024:fix/sync-retry-stall

Conversation

@Left024

@Left024 Left024 commented Jul 19, 2026

Copy link
Copy Markdown

Credits

This change was made by DeepSeek V4 Pro and has been manually tested by @Left024 on device — verified working correctly.

Problem

When background sync encounters an error (e.g., FreshRSS credentials expire, network issues), the SyncWorker enters infinite exponential backoff (capped at 5h). Users experience this as "auto-refresh stopped working completely" — it never recovers until manual refresh or app restart.

Additional issues:

  • After switching accounts, periodic task carries the old accountId but dispatches via "current account" type, causing permanent type-mismatch failure
  • ReaderWorker retries indefinitely when any article full-content fetch fails, blocking the entire POST_SYNC_WORK chain (KEEP policy)
  • Changes to sync interval / Wi-Fi-only / charging-only settings only update the database but never reschedule the periodic task

Root Cause

  1. SyncWorker: sync() returns Result.Retry() → WorkManager exponential backoff. Each retry doubles the wait, permanently suspending the normal sync interval rhythm
  2. Account mismatch: enqueuePeriodicWork bakes accountId into inputData, but doWork() uses rssService.get() (current account type) — cross-account dispatch fails with type checks
  3. ReaderWorker: any failed article fetch → infinite retry → POST_SYNC_WORK never completes → subsequent syncs can't enqueue new ReaderWorker/WidgetUpdateWorker
  4. Settings not applied: sync settings changes only write to DB; rescheduling only happens at app restart via initSync()

Changes

  • SyncWorker: capping retries at 3 attempts (1 initial + 2 backoff retries), then returning Result.Failure — for periodic tasks this triggers resetPeriodic(), allowing the next sync interval to proceed normally. Also injecting AccountService to dispatch by inputData.accountId instead of current account
  • ReaderWorker: capping retries to avoid blocking POST_SYNC_WORK chain indefinitely; missing full content will be re-fetched by the next sync cycle
  • AbstractRssRepository: extracting reschedulePeriodicWork(account) from initSync() for external reuse
  • AccountViewModel: calling reschedulePeriodicWork immediately on account switch and sync setting changes, no longer requires app restart
  • FeverRssService: changed Result.Failure() to Result.Retry() for behavior consistency with Local/GoogleReader

Additional Improvements (2026-07-22)

Based on testing and further analysis, these incremental changes enhance sync reliability and user experience:

SyncWorker & AbstractRssRepository

  • Multi-account initSync: initSync() now iterates all accounts (not just the current one), enqueuing periodic work for each — ensures all accounts get synced after app restart
  • Top-level exception guard: doWork() wrapped in try-catch to prevent unhandled exceptions from crashing the worker
  • accountId propagation: POST_SYNC_WORK chain passes accountId to ReaderWorker for correct account dispatch
  • Expedited one-shot syncs: one-time sync tasks requested with setExpedited() for faster execution
  • Schedule extraction: enqueuePeriodicWork schedule constraints extracted into a reusable builder

ReaderWorker

  • AccountService injected to resolve account type from inputData.accountId, with fallback to current account for backward compatibility with old tasks
  • Full-content fetch retry cap uses its own constant (decoupled from SyncWorker's retry limit)

Network Recovery (AndroidApp.kt)

  • Registered NetworkCallback that triggers a one-shot sync when network becomes available after a disconnect — prevents missed sync windows when offline

Battery Optimization (AndroidManifest + ContextExt + TroubleshootingPage)

  • Added REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission to both manifests
  • New isIgnoringBatteryOptimizations() and openBatteryOptimizationSettings() extension functions
  • Troubleshooting page now displays battery optimization status with a card; fixed crash on invalid nextScheduledMillis (shows "N/A")

Notifications (NotificationHelper.kt)

  • Multi-article notifications use InboxStyle to display article title list
  • setAutoCancel(true) for automatic dismissal
  • Notification IDs use deterministic hash (article.id.hashCode()) instead of random numbers

AccountViewModel

  • After account deletion, rebuilds periodic sync for the target account
  • Renamed local variable rssServicerssRepo to disambiguate from member property

Strings

  • Added battery optimization status strings (Chinese + English)

Files Updated (both commits combined)

  • .gitignore
  • app/src/googlePlay/AndroidManifest.xml
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt
  • app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt
  • app/src/main/java/me/ash/reader/domain/service/ReaderWorker.kt
  • app/src/main/java/me/ash/reader/domain/service/SyncWorker.kt
  • app/src/main/java/me/ash/reader/domain/service/WidgetUpdateWorker.kt
  • app/src/main/java/me/ash/reader/infrastructure/android/AndroidApp.kt
  • app/src/main/java/me/ash/reader/infrastructure/android/NotificationHelper.kt
  • app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt
  • app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt
  • app/src/main/java/me/ash/reader/ui/page/settings/troubleshooting/TroubleshootingPage.kt
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values/strings.xml

Left024 added 3 commits July 19, 2026 03:00
- SyncWorker: cap retry at 3 attempts, then fail to let periodic reset; use
  accountId from inputData to dispatch correct service type
- ReaderWorker: cap retry to avoid blocking POST_SYNC_WORK chain indefinitely
- AbstractRssRepository: extract reschedulePeriodicWork() for reuse
- AccountViewModel: reschedule periodic tasks immediately after account switch
  or sync setting change
- FeverRssService: use retry instead of failure for consistency
- SyncWorker.enqueuePeriodicWork: use CANCEL_AND_REENQUEUE for ENQUEUED state
  (only keep UPDATE for RUNNING), so that lastEnqueueTime is reset to current
  system time on every app start
- WidgetUpdateWorker.enqueuePeriodicWork: same treatment
- This prevents a one-time clock anomaly from permanently polluting the
  nextScheduleTimeMillis (symptom: always showing Aug 17 regardless of real date)
- AbstractRssRepository: clearKeepArchivedArticles 接受 accountId 参数;initSync 遍历所有账户独立调度
- SyncWorker: doWork 顶捕异常;POST_SYNC_WORK 传递 accountId;一次性任务 setExpedited;抽取 schedule 构建逻辑
- ReaderWorker: 注入 AccountService;从 inputData 读取 accountId 兼容旧任务回退
- AndroidApp: 注册 NetworkCallback,网络恢复时触发一次性同步
- AndroidManifest: 添加 REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 权限
- NotificationHelper: InboxStyle 展示标题列表;setAutoCancel;确定性通知 ID
- ContextExt: 新增电池优化状态检查与设置跳转扩展
- AccountViewModel: 删除账户后重建周期同步;修复局部变量命名歧义
- TroubleshootingPage: 新增电池优化状态卡片;修复 nextScheduledMillis 异常崩溃
- strings: 新增电池优化相关字符串(中英文)
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.

1 participant