Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Cancellation races can leak the session lock or duplicate history rows, while blocking Drive maintenance can outlast the sync deadline.
3 open issues
What changed in this PR
This PR hardens AppWatcher’s background update checks against stalls and adds diagnostics to investigate gaps in update notifications.
Changes:
- Adds bounded token requests, HTTP calls, session-lock waits, and sync execution, with cancellation handling.
- Records interrupted and abandoned syncs, exposes new history statuses, and logs session stages and worker stop reasons.
- Adds regression tests, increments
versionCodeto17205, and refreshes the release startup profile.
| File | Description |
|---|---|
app/src/test/java/com/anod/appwatcher/sync/UpdateCheckRegistrationTest.kt |
Tests timeout, cancellation, busy-session, and abandoned-history handling. |
app/src/test/java/com/anod/appwatcher/accounts/AuthTokenBlockingTest.kt |
Tests token interruption and session-wait timeout recovery. |
app/src/release/generated/baselineProfiles/startup-prof.txt |
Refreshes the generated release startup profile. |
app/src/main/java/com/anod/appwatcher/sync/UpdateCheck.kt |
Adds sync deadlines, interruption recording, and stage diagnostics. |
app/src/main/java/com/anod/appwatcher/sync/SyncWorker.kt |
Logs worker lifecycle and supplies stop reasons. |
app/src/main/java/com/anod/appwatcher/sync/SchedulesHistoryScreen.kt |
Displays interrupted and busy-session statuses. |
app/src/main/java/com/anod/appwatcher/database/SchedulesTable.kt |
Adds unfinished-history lookup and conditional interruption updates. |
app/src/main/java/com/anod/appwatcher/database/entities/Schedule.kt |
Defines the new failure statuses. |
app/src/main/java/com/anod/appwatcher/CrashlyticsExceptionFilter.kt |
Keeps interruption diagnostics reportable. |
app/src/main/java/com/anod/appwatcher/AppModule.kt |
Configures a two-minute OkHttp call timeout. |
app/src/main/java/com/anod/appwatcher/accounts/AuthTokenBlocking.kt |
Bounds token waits and makes blocking requests interruptible. |
app/src/main/java/com/anod/appwatcher/accounts/AuthAccountInitializer.kt |
Adds timed session acquisition and holder-stage diagnostics. |
app/build.gradle.kts |
Increments the release build number. |
Review effort: Balanced
| withTimeoutOrNull(timeouts.syncMillis) { | ||
| performSerialized(manualSync, stopReason) | ||
| } ?: -1 |
| val recorded = if (schedule.id == -1L) { | ||
| SchedulesTable.Queries.save(interrupted, database) | ||
| true |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved concurrency and cancellation issues can leave callers suspended indefinitely, duplicate sync history, and block UI work, while a missing test dependency causes a timeout.
6 open issues
Bound and cancel Drive maintenance operations Recorder monitor blocks UI and scheduling on slow disk operations · New Full command queue can suspend callers indefinitely during shutdown · New Mutex wait bypasses advertised session timeout and history status · New Test fixture lacks disabled DiagnosticRecorder registration · New Make schedule insert and ID assignment cancellation-safe
1 resolved since last review
Review effort: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Account switches can retain consent and buffered events, while recorder shutdown, lock-held disk I/O, and unbounded Drive transfers can hang operations or delay cancellation.
7 open issues
Bound and cancel Drive maintenance operations Avoid holding the recorder monitor during filesystem I/O · New Clear consent and invalidate buffered records on account changes · New Bound and cancel Drive maintenance transfers · New Mutex wait bypasses advertised session timeout and history status Full command queue can suspend callers indefinitely during shutdown Make schedule insert and ID assignment cancellation-safe
2 resolved since last review
Review effort: Balanced
| withTimeoutOrNull(timeouts.syncMillis) { | ||
| performSerialized(manualSync, stopReason) | ||
| } ?: -1 |
There was a problem hiding this comment.
🟡 Changes recommended
Cancellation races can create duplicate history rows while leaving originals unfinished, and recorder shutdown can indefinitely hang callers waiting on a full command queue.
7 open issues
Bound and cancel Drive maintenance operations Publish storage state after each flushed batch · New Clear persisted consent when switching diagnostic accounts · New Bound and cancel Drive maintenance transfers Clear consent and invalidate buffered records on account changes Full command queue can suspend callers indefinitely during shutdown Make schedule insert and ID assignment cancellation-safe
2 resolved since last review
Review effort: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Cancellation races can create duplicate schedule rows, leave original rows unfinished, and clear update status before users are notified, while interrupted sign-in can skip the completion broadcast.
5 open issues
4 resolved since last review
Review effort: Balanced
| withTimeoutOrNull(timeouts.syncMillis) { | ||
| performSerialized(manualSync, stopReason) |
| AppLog.i("Perform Google Drive sync", "UpdateCheck") | ||
| val googleAccount = signIn.signInLocked() | ||
| // Silent sign-in blocks on a Play services task without a timeout; let the sync cap interrupt it. | ||
| val googleAccount = runInterruptible(Dispatchers.IO) { signIn.signInLocked() } |
- Bound AccountManager token waits and make them interruptible - Add OkHttp call timeout, session wait timeout, and overall sync timeout - Record interrupted/busy syncs in history and report them to Crashlytics - Log sync stages, session holder, and WorkManager stop reason - Bump versionCode to 17205 and regenerate baseline profiles Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Remove timed session acquisition and busy status; keep single gate, bounded token/HTTP calls and active sync cap - Add account-allowlisted persistent diagnostic recorder (30 days, 10 MiB) with Settings toggle, share and delete - Record scheduler, worker, session, sync stage and startup state events - Pass diagnostic recorder explicitly to sync and settings components Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Bind diagnostics consent to the account hash in Preferences - Make recorder operations non-throwing and self-refreshing - Reduce SettingsViewModel diagnostics handling and reuse StartActivity - Fix missing diagnostics dependency in account selection test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Diagnostics now tee AppLog output to a local file while the allowlisted account has opted in, instead of recording structured events from sync, session and notification code. The tee is installed only when enabled, so disabled or disallowed builds keep the plain logger. Removes abandoned-sync detection; interrupted runs are still recorded. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Remove opt-in diagnostics (now an independent PR against master) - Require explicit session owner, sync timeouts, and log release before unlock - Make stage updates non-suspend and label chunks by position - Run Drive silent sign-in interruptibly so the sync cap can stop it Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
6b92c69 to
7c5d900
Compare
Wrap the sync body in withTimeout inside performSerialized so a real timeout is detected through TimeoutCancellationException instead of a duration heuristic. The heuristic compared against a clock that starts after the timer, so a timeout could be labelled a cancellation. The interruption is still recorded under NonCancellable and a timeout still returns -1. In the stalled-sync test, use the 300 ms capped UpdateCheck only for the stalling sync. The recovery sync now runs under the regular cap and no longer times out on slow CI runners. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Cancellation handling remains unsafe: silent sign-in can turn interruption into an ordinary failure, history insertion races can create duplicate and unfinished rows, and blocking Drive transfers can exceed the six-minute cap.
6 open issues
Cancellation can lose notifications after update commit Bound and cancel Drive maintenance operations Bound Drive transfers with cancellation-aware transport deadlines · New Interrupted sign-in bypasses timeout cancellation handling Bound and cancel Drive maintenance transfers Make schedule insert and ID assignment cancellation-safe
Review effort: Balanced
| withTimeout(timeouts.syncMillis) { | ||
| syncSerialized(schedule, manualSync) | ||
| } |


Problem
Beta users (and the maintainer) stop receiving update notifications until the app is opened. On-device refresh history shows every multi-day gap starting with a scheduled sync row that started but never finished.
Likely cause (not yet confirmed): a scheduled sync blocks forever on AccountManager
getAuthToken(...).result(no timeout, not cancellable) while holding thePlaySessionCoordinatormutex, so every later sync waits on the lock.Fixes
runInterruptible;OperationCanceledExceptioncaused by cancellation is surfaced as cancellation.Tasks.awaitwith no timeout) runs interruptibly so the sync cap can stop it.callTimeoutof 2 minutes (DFE calls are enqueued and also cancelled with the coroutine).Diagnostics
failureKindlabel.PlaySession,UpdateCheck,SyncWorker, andNotification.Opt-in on-device diagnostic logs moved to #187.
Release
versionCode17205; release baseline profiles regenerated on a physical device.Tests
:app:testDebugUnitTest235/235 passing;:app:assembleDebugand main ktlint clean.