Skip to content

fix(aw-sync): skip broken peers in sync_run instead of aborting the pass - #703

Merged
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/aw-sync-skip-unreadable-peers
Sep 17, 2026
Merged

ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/aw-sync-skip-unreadable-peers

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Fixes the daemon-path half of #688.

The supervised aw-sync daemon walks remotes inside sync_run. A single unreadable peer used ? and skipped every peer after it, then exited non-zero and burned aw-qt/aw-tauri's restart budget. That is the path users actually run; #685 (host-layout pull_all) is parked for v0.14.0, so #697's per-peer continue in pull_all does not cover the daemon.

Change

  • sync_run skips an unreadable peer db instead of return Err.
  • New pull_from_remotes: warn+continue on each remote's sync_datastores failure. Partial skip is Ok; attempted > 0 && succeeded == 0 is still Err so a down destination is not reported as success.

Tests

  • broken_peer_does_not_skip_healthy_sibling — broken peer listed first, healthy sibling still imports.
  • all_peers_failing_is_still_err — total failure stays Err.

Not this PR

Does not close #688 on its own.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Review guide (auto-posted)

Structured warm-up for reviewers — what changed, what to run, where to look.

Key files

  • aw-sync/src/sync.rs (+111/-5, Δ116)

Suggested verification

  • Run the repo's usual CI-equivalent checks locally

Known risks / watch points

  • No automatic high-risk tags; use file list + diff for judgment.

Suggested review focus

  • Confirm behavior matches the PR description acceptance criteria.
  • Skim the largest diffs first (listed above).
  • If CI is green, spot-check the highest-risk paths called out here.

Generated by scripts/github/pr-warmup-review-guide.py for #703.

@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob Content is right — this closes the residual I named on #700: a peer that fails to open for a non-version reason now skips instead of aborting the pass, and the same attempted > 0 && succeeded == 0 → Err rule as #697 keeps total failure visible to the supervisor. With this, #688's per-peer isolation is complete across all three layers (open, per-remote sync, per-bucket).

Sequence: last, after #697. It edits the same sync_run remote loop that #700 and #697 touch. Pairwise it is clean against both, but that was also true of #697/#699 before #698 squash-merged, and both conflicted afterwards. Expect to rebase once #700 and #697 are in; merge on green after that. Does not change Erik's current order (#700#678#697).

@greptile-apps

greptile-apps Bot commented Sep 16, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the latest change fully removes Push-only synchronization’s dependency on peer database readability.

Findings

  1. P1 Push depends on peer reads

Summary

This PR isolates peer failures during daemon synchronization while preserving total-failure reporting.

  • Skips individual unreadable peer databases without preventing healthy peers from being processed.
  • Continues pulling from remaining peers after an individual synchronization failure.
  • Keeps all-peer open or pull failures visible as errors.
  • Prevents Push-only passes from opening peer databases, resolving the remaining previous finding.

Reviews (5) · Last reviewed commit: "fix(aw-sync): only open peer datastores ..."

Comment thread aw-sync/src/sync.rs Outdated
Comment thread aw-sync/src/sync.rs Outdated
@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 52.94118% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.65%. Comparing base (656f3c9) to head (789b13c).
⚠️ Report is 124 commits behind head on master.

Files with missing lines Patch % Lines
aw-sync/src/sync.rs 52.94% 32 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #703      +/-   ##
==========================================
+ Coverage   70.81%   79.65%   +8.83%     
==========================================
  Files          51       74      +23     
  Lines        2916     7853    +4937     
==========================================
+ Hits         2065     6255    +4190     
- Misses        851     1598     +747     

☔ View full report in Codecov by Harness.
📢 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.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Got it. Stays last, after #697.

Not merging now. Order remains #700#678#697. Once #700 and #697 are in I'll rebase this onto master and merge on green.

Greptile's dest-failure P1 is the #697 rule you just signed off — leaving it. The open-failure counting P1 is the rebase against #700: that PR's open_peer_datastore Err arm still return Err; this PR turns it into skip. I'll fold that in on the rebase, not ahead of the stack.

@TimeToBuildBob

TimeToBuildBob commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

Safe to merge — 1 P1 disposed (fixed in 789b13c)

Updated after inline dispositions on finding threads — this is the current state; the verdict below is frozen at review time and is kept as the historical record of that pass.

Finding disposition
Finding Severity State
aw-sync/src/sync.rs:351 P1 fixed in 789b13c
aw-sync/src/sync.rs:751 P2 superseded by latest review (not reproduced)
aw-sync/src/sync.rs:114 P2 rejected
aw-sync/src/sync.rs:378 P2 superseded by latest review (not reproduced)
aw-sync/src/sync.rs:920 P2 rejected
aw-sync/src/sync.rs:1364 P2 fixed in 9b49d4a
aw-sync/src/sync.rs:124 P2 rejected
aw-sync/src/sync.rs:353 P2 accepted-tradeoff
aw-sync/src/sync.rs:1228 P2 other

This PR changes aw-sync's sync_run so that a single unreadable or incompatible peer database no longer aborts the whole sync pass. It adds open_peer_datastores() and pull_from_remotes() helpers that skip failing peers while recording them on the report, and only return Err when every peer fails. It also adds a new test module peer_isolation_tests covering partial and total failure cases.

Needs a look — P2 only

Confidence 4/5

2 findings · ⚠️ 2 P2 — non-blocking (round cap reached)

⚠️ P2 medium · ℹ️ non-blockingaw-sync/src/sync.rs:353

In open_peer_datastores(), the condition if opened.is_empty() && failures > 0 treats a pass where every discovered peer is version-incompatible (OpenedPeer::Incompatible) as success, because failures only counts hard open errors. The PR description and comments say a total open failure should be Err so a broken folder is not reported as a clean empty pass, but an all-incompatible folder is reported as Ok with an empty ds_remotes, and sync_run then proceeds to push (in Both mode) and returns Ok. This contradicts the stated intent in the comment on lines 120-122 and the PR description's 'only a total open failure is fatal' — version-incompatible peers are skipped, not counted as failures, so a folder where every peer is incompatible is silently treated as a clean pass. The observable consequence is that a user with an all-incompatible sync folder gets a successful sync report with no peers imported, rather than an error indicating the folder is unusable.

if opened.is_empty() && (failures > 0 || incompatible_count > 0) {

How this was verified: Checked the loop: Incompatible arm does not increment failures, only the Err arm does. The test version_skip_plus_open_failure_counts_only_failures explicitly asserts that version-mismatch skips are not counted as failures, confirming the behavior.

⚠️ P2 medium · ℹ️ non-blockingaw-sync/src/sync.rs:1228

The test broken_peer_does_not_skip_healthy_sibling uses unreadable_peer() which creates a Datastore whose parent directory does not exist. The comment says Datastore::new succeeds lazily but sync_datastores fails. However, the test then passes this broken Datastore as a remote and expects pull_from_remotes to skip it. The test does not actually verify that the broken peer's failure is recorded on the report (it passes record_peers=false), and it does not verify that the broken peer's sync_datastores actually failed — it only checks that the healthy sibling imported. If unreadable_peer() somehow succeeded (e.g., the temp dir got created by another test), the test would still pass because the healthy sibling imports regardless. The test is not hermetic in the sense that it depends on the parent directory not existing, which could be affected by other tests or environment. This is a test-defect: the test's assertion would pass even if the broken peer was actually readable, so it does not pin the skip behavior.

How this was verified: The test creates broken via unreadable_peer() which uses a unique path with process id and nanos, so the parent dir should not exist. But the test does not assert that the broken peer was skipped; it only asserts the healthy sibling imported. If the broken peer were readable, the test would still pass.

1 advisory finding (summary-only, not scored)

These P2 guard, heuristic, trade-off, or documentation claims are retained for judgment without opening review threads.

⚠️ P2 mediumaw-sync/src/sync.rs

This is a fix(...) PR but no test files are included in the diff. Erik's feedback: 'where is the repro & fixes they are supposed to catch' (gptme#3441), 'that measurement should come with a regression test' (gptme#3446). Add a test that would have caught this bug. (Advisory: Erik merged all such PRs but consistently requested tests.)

Add a test file that reproduces the bug before the fix and passes after it.

How this was verified: static preflight: fix-commit + touched-files scan (rule 7)

Files changed (1) — the diff as I read it
  • aw-sync/src/sync.rs — Replaces the per-peer open and pull loops in sync_run with open_peer_datastores() and pull_from_remotes() that skip failing peers, and adds peer_isolation_tests.
Previous review passes
commit score findings engine when
17fc4b909cc3 4/5 1 llm 2026-09-16 11:23 UTC
fcaba7f8d7f2 5/5 0 llm 2026-09-16 12:22 UTC
ac424d49d287 4/5 3 llm 2026-09-16 15:16 UTC
14d7f9a963e2 5/5 0 llm 2026-09-16 15:50 UTC
a77559672ec3 4/5 1 llm 2026-09-17 09:41 UTC
9b49d4a3233a 3/5 3 llm 2026-09-17 11:02 UTC

Reviewed 789b13cf28e5 · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 27s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

Comment thread aw-sync/src/sync.rs Outdated
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Closed the open-failure counting hole in fcaba7f: if every discovered peer fails to open, pull is now Err (zero peers stays a no-op). Isolation tests are hermetic.

Greptile dest-failure P1 stays: that is the #697 total-failure rule you signed off.

Still last in the stack. Not merging until #700#678#697 are in; then rebase and merge on green.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click.

This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Rebased onto master after #700 landed (this was DIRTY).

Folded the #700 residual: open_peer_datastores now walks open_peer_datastore (read-only, version-mismatch skip). A non-version open failure is skip-and-continue; if every discovered peer fails to open, pull is still Err. Version-mismatch-only stays a no-op.

Isolation tests updated for the read-only open (missing file is a hard fail; mixed-open creates a checkpointed current-version db). Local peer_isolation_tests + pull_only_staging_tests green.

Still last in the stack — not merging until #697 is in. Order remains #700#678#697; rebase again if #697 conflicts, then merge on green.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw-sync/src/sync.rs Outdated
Comment thread aw-sync/src/sync.rs Outdated
Comment thread aw-sync/src/sync.rs Outdated
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Addressed the in-band AI review P2s on ac424d4:

  • Counted only hard open failures in the all-fail error (14d7f9a). Version-mismatch skips no longer inflate paths.len().
  • Rejected the ds_localremote leak: Drop already closes the worker (documented in sync_run; poll Err → quit).
  • Rejected the mixed_open_keeps_the_readable_peer assertion nit — remotes.len() == 1 is the contract.

Local in-band review of 14d7f9a is 5/5. Still last in the stack; not merging until #697 is in.

@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob resolve conflicts

@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob CONFLICTING again — #699 and #678 both landed this morning (master is 3d3b9dd). Rebase onto current master; nothing else has moved and nothing else will before these three go in, so this is the last rebase. Order after rebase: #697#703 (same sync_run region) → #702 any time.

@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob #697 just merged, so the rebase target is now d1a61b1, not 3d3b9dd. This is the last sync PR in the v0.14.0 sequence — master will not move again before it lands (#702 and #706 don't touch sync_run), so one rebase onto d1a61b1 is final. After it: verify the open-failure skip and the attempted > 0 && succeeded == 0 → Err rule still compose with #697's identical rule in sync_datastores/pull_all — you don't want the same total-failure check twice in one pass.

@TimeToBuildBob
TimeToBuildBob force-pushed the fix/aw-sync-skip-unreadable-peers branch from 14d7f9a to eecc50f Compare September 17, 2026 09:19
@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob eecc50f landed on 3d3b9dd, not current master — #697 merged as d1a61b1 in between, so this is still CONFLICTING (diff vs master is +376/−635, the deletions being #697's changes absent from the branch). One more rebase, onto d1a61b1; nothing else will move.

While rebasing, resolve the duplication rather than carrying it: this PR adds attempted > 0 && succeeded == 0 → Err inside a new pull_from_remotes, and #697 already landed the same rule in pull_all (sync_wrapper.rs) and sync_datastores. Keep one total-failure check per pass — the natural home is wherever the peer loop ends up after the rebase — so a pass with zero successes returns Err exactly once and a partial pass never does. Content otherwise verified: unreadable-peer skip and per-remote skip are both present.

Rebased onto post-ActivityWatch#699/ActivityWatch#678 master (SyncReport/PeerReport world): open and
pull failures per peer are now recorded on the report and skipped; only a
total failure (every discovered peer failed) is Err, so one unreadable or
failing peer no longer aborts the whole pass (ActivityWatch#688).

Co-Authored-By: Bob
Git-Session-Id: 2d23d1d2-e3df-5c27-97f3-ef94f342ad09
@TimeToBuildBob
TimeToBuildBob force-pushed the fix/aw-sync-skip-unreadable-peers branch from eecc50f to a775596 Compare September 17, 2026 09:23
@ErikBjare

Copy link
Copy Markdown
Member

Verified on a775596: on d1a61b1, one commit, MERGEABLE; version-mismatch skip (:299), unreadable-peer skip (:337) and per-remote skip (:389) all present.

Retracting the duplication flag — the three attempted > 0 && succeeded == 0 checks are three scopes that nest correctly, not one rule repeated:

  • sync_datastores — bucket scope (all buckets of one peer failed)
  • pull_from_remotes — remotes within one sync_run; on the pull_all path each run sees exactly one remote
  • pull_all — hosts scope; treats the inner Err as one failed peer and only errors when every host failed

Net behaviour: partial failure never returns Err, total failure returns Err exactly once, and the advanced sync_run-against-root path (which never enters pull_all) keeps its own guard. That is the #688 contract. Nothing further — ready for Erik on green. Last sync PR in the v0.14.0 sequence.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Rebased onto current master (d1a61b1, after #697/#699/#678 landed) as a single squashed commit (a775596). The #688 semantics are re-expressed in the post-#699 report world: per-peer open failures and pull failures are recorded on the report (PeerReport::failed) and skipped; only total failure — every discovered peer failed to open, or every pull failed — is Err, so the daemon still fails loud on a broken folder without aborting the pass on one bad peer. Peer isolation tests updated and passing locally (43/43 in aw-sync --lib). CI is running on the new head.

Comment thread aw-sync/src/sync.rs Outdated
Use NEWEST_DB_VERSION + 1000 instead of hardcoded 4 so the test cannot
go stale when the supported schema version reaches the hardcoded value.
Addresses our AI review P2 (fp 4bf1d7a1ac8b).

Git-Session-Id: 515fe2f5-05bc-5268-82e6-bac7396140ff
@ErikBjare

Copy link
Copy Markdown
Member

9b49d4a verified: test-only — the version-skip test now uses NEWEST_DB_VERSION + 1000 instead of a hardcoded 4 that would go stale with the next schema bump. Three skips and the three-scope total-failure rules unchanged from the verified a775596. Still MERGEABLE against post-#706 master; no further rebase. Merge on green — last sync PR.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw-sync/src/sync.rs Outdated
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Merge attempted via the gated self-merge path — blocked: the bot token has pull-only access to this repo (collaborator-permission probe confirms 403). Everything else is green: CI 6/6 success, Greptile 5/5, all review threads resolved except our own P2 which is fixed in 9b49d4a (test-only, NEWEST_DB_VERSION + 1000) with the thread reply posted. Needs your merge click, Erik.

Comment thread aw-sync/src/sync.rs
Comment thread aw-sync/src/sync.rs Outdated
Push-only passes never read peer datastores, so opening them there was
wasted work and made an all-unreadable-peer folder abort the pass before
anything was pushed. Gate open_peer_datastores on Pull/Both; push mode
now proceeds regardless of peer database state.

Closes the Greptile P1 on 9b49d4a and the push-mode visibility P1 from
the in-band review (with the gate, peer opens only ever run with
record_peers=true, so incompatible peers are always on the report).

Git-Session-Id: 63c563f9-1bf6-5fa9-a53b-c51caa2275d0
@ErikBjare

Copy link
Copy Markdown
Member

789b13c verified: Push-only passes no longer open peer datastores — the mirror of #698 (pull-only never stages), and correct: a push only touches the local staging db, so an unreadable or incompatible peer must not be able to abort or taint it. Three skips and the three-scope total-failure rules unchanged. MERGEABLE against current master. Merge on green — this is the last sync PR, and it needs Erik's click (pull-only token, as your 11:04 probe found).

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

The fresh review on 9b49d4a surfaced one real P1 (push-only passes were opening peer datastores and could abort before pushing anything) — fixed in 789b13c: open_peer_datastores now runs only for Pull/Both passes; push mode never touches peer databases. The in-band review P1 (invisible all-incompatible pass) is resolved by the same gate, its P2 was a self-declared duplicate, and the earlier hermetic-test P2 was already fixed in 9b49d4a. All four threads closed; 43/43 lib tests pass. Re-review triggered on the new head.

Still needs your merge click when green — the bot token remains pull-only on this repo.

Comment thread aw-sync/src/sync.rs
Comment thread aw-sync/src/sync.rs
@ErikBjare
ErikBjare merged commit b0fab73 into ActivityWatch:master Sep 17, 2026
8 checks passed
@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob address in follow-up if needed

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Disposition of the three threads left open when this merged — thanks for the click.

1. Greptile P1 — "Push depends on peer reads" (fixed, thread just stale). Fixed in 789b13c: open_peer_datastores now runs only for Pull/Both, so a push-only pass never opens peer databases. The thread is outdated; resolving it.

2. c703df198fee (P2, line 353) — "all-incompatible folder reported as a clean pass" → dismissed. This reads the intent backwards. Version-incompatible peers are deliberately skipped, not fatalsync.rs:114-123 says so explicitly, and the #688 lesson is that routing a per-peer condition into a non-zero exit is what burns aw-qt/aw-tauri's restart budget and then disables sync. An all-incompatible folder is a benign steady state (the peer is on a different schema version), not a broken one, so it must not be Err. The visibility requirement is met a different way: the skipped peers are recorded on the report (PeerReport::skipped with the reason), which is what aw-sync status renders. Making this Err would re-open #688.

3. 557933a72bea (P2, line 1228) — test does not pin the recording path → valid, deferred. Correct in substance: all eight test call sites pass record_peers=false, so PeerReport::failed/skipped are never asserted. The skip behaviour is pinned; the visibility half of the contract is not. It's test-only, so rather than open a PR against the current layout it's filed as a local task (aw-sync-688-report-visibility-test-coverage) to pick up after the v0.14.0 cut, per Erik's "queue waits for the cut" on ActivityWatch/activitywatch#1445.

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.

aw-sync: per-peer and per-bucket errors abort the whole sync pass (and burn the supervisor's restart budget)

2 participants