Skip to content

fix(sync): keep the in-flight guard while a cancelled sync is running - #339

Open
0xbrayo wants to merge 2 commits into
ActivityWatch:masterfrom
0xbrayo:fix/sync-cancel-inflight-guard
Open

0xbrayo wants to merge 2 commits into
ActivityWatch:masterfrom
0xbrayo:fix/sync-cancel-inflight-guard

Conversation

@0xbrayo

@0xbrayo 0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member

Part of #333.

SyncInterface.cancel() (called when WorkManager stops a SyncWorker) cleared the shared syncInFlight guard unconditionally. Interrupting the executor thread does not stop the native syncBoth call, so the cancelled sync kept running while the next sync (from the Handler chain, the alarm, or a manual tap) was allowed to start alongside it, with both writing to the same sync directory and database.

Change

  • cancel() clears syncInFlight only when shutdownNow() returned a task that never started. Only then will that task's completion callback, which normally clears the guard, never run. That case was the original reason for clearing it here.
  • A started task keeps the guard until its completion callback clears it, which happens once the native call returns.

Testing

  • ./gradlew :mobile:testStandardDebugUnitTest passes.
  • SyncInterface loads the native aw-sync library, which isn't available to JVM unit tests, so this has no new unit test.

@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 actionable new issues remain.

Summary

This PR keeps syncInFlight set while a cancelled native sync is still running.

  • cancel() releases the guard directly only when the executor removes an unstarted task.
  • Rejected submissions now report cancellation through the completion callback.
  • The previous submission-race finding is fixed. The thread is unnumbered, so it has no entry in previousFindings.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["cancel()"] --> B["shutdownNow()"]
  B --> C{"Queued task removed?"}
  C -- Yes --> D["Clear syncInFlight"]
  C -- No --> E{"Submission rejected?"}
  E -- Yes --> F["Post cancellation callback"]
  E -- No --> G["Running task finishes"]
  G --> H["Post completion callback"]
  F --> D
  H --> D
Loading

Reviews (2) · Last reviewed commit: "fix(sync): release the guard when a canc..." · Reviewed by Greptile

Comment thread mobile/src/main/java/net/activitywatch/android/SyncInterface.kt
cancel() cleared syncInFlight unconditionally. Interrupting the executor
does not stop the native syncBoth call, so a new sync could start while
the cancelled one was still writing. Only clear the guard when
shutdownNow() removed a task that never started, since only then will
its completion callback never run.
@0xbrayo
0xbrayo force-pushed the fix/sync-cancel-inflight-guard branch from d0349d6 to 8eb1bec Compare October 8, 2026 19:37
If cancel() runs between publishing activeExecutor and submitting the
task, shutdownNow() has nothing to return and the later execute() is
rejected, so no completion callback ever cleared syncInFlight. Report
the rejected submission through the callback, which releases it.
@0xbrayo

0xbrayo commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

🤖 Claude, on behalf of @0xbrayo

@greptile review

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