Skip to content

fix(sync): stop syncing twice per interval - #340

Merged
ErikBjare merged 5 commits into
ActivityWatch:masterfrom
0xbrayo:fix/sync-dedupe-schedulers
Oct 10, 2026
Merged

ErikBjare merged 5 commits into
ActivityWatch:masterfrom
0xbrayo:fix/sync-dedupe-schedulers

Conversation

@0xbrayo

@0xbrayo 0xbrayo commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Part of #333.

SyncScheduler runs an in-process Handler chain every 15 minutes and also registers an AlarmManager fallback with the same 15-minute interval. syncInFlight stops the two from overlapping, but not from running back to back, so most cycles did two full syncs and two SAF mirrors.

Change

  • Alarm skips recent syncs: SyncAlarmReceiver skips the fallback while the last sync is less than one interval old, i.e. while the Handler chain is on schedule. If the service was killed, the last sync goes stale and the alarm takes over as before.
  • Unique alarm work: the alarm enqueues unique work (KEEP), so a slow worker from the previous alarm can't be stacked with a new one.
  • Single Handler chain: the chain calls removeCallbacks(syncRunnable) before every repost. Before, a sync that completed after a stop()/start() reposted alongside the new chain and left two chains running.

Testing

  • New SyncSchedulerTest covers the skip decision: recent sync, stale or missing sync, and a completion time in the future after a clock step.
  • ./gradlew :mobile:testStandardDebugUnitTest passes.

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issue remains.

Summary

Prevents the Handler and fallback alarm from repeating scheduled syncs.

  • Records Handler passes at both start and completion.
  • Checks again when SyncWorker starts, covering delayed workers.
  • Uses unique alarm work and removes pending callbacks before reposting.

The previous threads are unnumbered, so they receive no previousFindings entries. Their reported issues are addressed in the current code.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Fallback alarm] --> B{Recent Handler pass?}
  B -->|Yes| C[Skip]
  B -->|No| D[Enqueue unique worker]
  D --> E{Recent Handler pass now?}
  E -->|Yes| C
  E -->|No| F[Sync and mirror]
  G[Handler pass starts] --> H[Record pass time]
  H --> I[Sync or skip overlapping call]
  I --> J[Record completion time]
  J --> K[Schedule next Handler pass]
Loading

Reviews (7) · Last reviewed commit: "fix(sync): recheck the Handler pass when..." · Reviewed by Greptile

Comment thread mobile/src/main/java/net/activitywatch/android/SyncScheduler.kt Outdated
The in-process Handler chain and the AlarmManager fallback both fired
every 15 minutes, so each cycle could run two full syncs and SAF
mirrors. The alarm now skips when a sync completed within the last half
interval, and enqueues unique work so alarm workers can't stack. The
Handler chain removes any pending runnable before reposting, so a sync
completing after stop()/start() can't leave two chains running.
@0xbrayo
0xbrayo force-pushed the fix/sync-dedupe-schedulers branch from fd4a25f to 72351a8 Compare October 8, 2026 19:37
Half an interval was too short: the Handler syncs every 15 minutes, so
the alarm firing ~14 minutes after a Handler sync still ran a second
full sync and mirror. Skip while the last pass is under one interval
old, i.e. while the Handler chain is on schedule.
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

Comment thread mobile/src/main/java/net/activitywatch/android/SyncAlarmReceiver.kt Outdated
With the last completed sync of any kind as the signal, a dead service
let the fallback's own sync suppress the next alarm, stretching the
fallback cadence to ~30 minutes. Record the Handler chain's passes in
their own preference and decide on those alone.
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

Comment thread mobile/src/main/java/net/activitywatch/android/SyncScheduler.kt Outdated
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

A Handler pass skipped because the fallback was already syncing still
schedules the chain's next pass, but left the recorded time stale, so
the following alarm could sync just before that pass. Record every
Handler pass, synced or skipped.
@0xbrayo
0xbrayo force-pushed the fix/sync-dedupe-schedulers branch from 7150018 to a67f3a8 Compare October 8, 2026 20:58
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

Comment thread mobile/src/main/java/net/activitywatch/android/AWPreferences.kt
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

Comment thread mobile/src/main/java/net/activitywatch/android/SyncAlarmReceiver.kt
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

An alarm delivered just before a Handler pass recorded it could queue a
worker that started right after that pass and synced again. Record the
pass when it starts as well as when it ends, and repeat the redundancy
check in SyncWorker before syncing.
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

@ErikBjare
ErikBjare merged commit e223e20 into ActivityWatch:master Oct 10, 2026
9 checks passed
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.

2 participants