fix(aw-sync): make default daemon use host-layout pull/push - #685
TimeToBuildBob wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #685 +/- ##
==========================================
+ Coverage 70.81% 78.90% +8.08%
==========================================
Files 51 72 +21
Lines 2916 6888 +3972
==========================================
+ Hits 2065 5435 +3370
- Misses 851 1453 +602 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…arnings `aw-sync daemon` can walk a configured sync dir and stay silent when it finds nothing to pull, which is indistinguishable from a working setup. This does not change which remotes are pulled (ActivityWatch#682 / ActivityWatch#685). It makes the miss diagnosable: - classify both 2-level (`{device_id}/*.db`) and 3-level (`{hostname}/{device_id}/*.db`) layouts without opening sqlite - warn! on pull when zero remotes are found, with skip reasons - `aw-sync status` prints every entry, inspects peer dbs read-only (no WAL sidecars), and flags duplicate device_id, hostname mismatch, unpublished staging, and unimported peers ActivityWatch#684 Git-Session-Id: 375e1ec2-04d0-5884-9beb-cc5cd9c704c6
|
CI-green and mergeable — 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 Reviewed all three PRs together. The fix itself is right — Three problems with the stack, though, and they all land on this PR. 1. The merge-order claim doesn't holdFrom your comment on #684: "#685 is the actual pull fix; #687 is the doctor. They touch the same files in different hunks and should merge either order." Test-merging every pair at their current heads: Reproduce with: git fetch origin pull/685/head:pr-685 pull/686/head:pr-686 pull/687/head:pr-687
git worktree add --detach /tmp/mt pr-685 && cd /tmp/mt
git merge --no-commit --no-ff pr-687 # CONFLICT
2. This PR's
|
d6b4f80 to
cd4facf
Compare
`aw-sync daemon` can walk a configured sync dir and stay silent when it finds nothing to pull. This does not change which remotes are pulled (ActivityWatch#682 / ActivityWatch#685). It makes the miss diagnosable: - 3-level peers come from list_remote_dbs + select_remote_dbs_by_device_id (same pair pull_all uses), so duplicate device_id reporting matches pull - status-only overlay on top: 2-level leftovers, unrecognised entries, own-staging vs peer, SyncLayout - warn! on pull when zero remotes are found, with skip reasons - `aw-sync status` inspects peer dbs read-only (no WAL sidecars) Stacked on ActivityWatch#686. ActivityWatch#684 Git-Session-Id: 1673d3c9-c19e-5588-bfa5-96436fd680e8
|
@TimeToBuildBob The restack is right, and it did what it needed to:
One leftover from the rebase:
Deleting the trio leaves two walkers for two genuinely distinct code paths:
Two is defensible and worth a comment saying why they differ. Three, one of which is unreachable, is not. CI is still in flight on both as I write this. Once it is green and (#687 still adds |
|
@ErikBjare Restacked as you asked. This PR is now
On the CI is green except coverage still pending. Still pull-only on this repo. ActivityWatch/aw-android#272 is still the source of the duplicate folders #686 defends against. That's next after this stack. |
…arnings `aw-sync daemon` can walk a configured sync dir and stay silent when it finds nothing to pull, which is indistinguishable from a working setup. This does not change which remotes are pulled (ActivityWatch#682 / ActivityWatch#685). It makes the miss diagnosable: - classify both 2-level (`{device_id}/*.db`) and 3-level (`{hostname}/{device_id}/*.db`) layouts without opening sqlite - warn! on pull when zero remotes are found, with skip reasons - `aw-sync status` prints every entry, inspects peer dbs read-only (no WAL sidecars), and flags duplicate device_id, hostname mismatch, unpublished staging, and unimported peers ActivityWatch#684 Git-Session-Id: 375e1ec2-04d0-5884-9beb-cc5cd9c704c6
`aw-sync status` was a third directory walk beside list_remote_dbs (ActivityWatch#686) and collect_db_files (ActivityWatch#685). 3-level peers now come from the same list_remote_dbs + select_remote_dbs_by_device_id pair pull_all uses, so duplicate-device_id "not pulled" matches the pull path. Leftover 2-level / unrecognised entries sit on top of that list. ActivityWatch#687 Git-Session-Id: eca21d8a-f6da-5096-b28f-783778f4c09f
cd4facf to
523f202
Compare
…arnings `aw-sync daemon` can walk a configured sync dir and stay silent when it finds nothing to pull, which is indistinguishable from a working setup. This does not change which remotes are pulled (ActivityWatch#682 / ActivityWatch#685). It makes the miss diagnosable: - classify both 2-level (`{device_id}/*.db`) and 3-level (`{hostname}/{device_id}/*.db`) layouts without opening sqlite - warn! on pull when zero remotes are found, with skip reasons - `aw-sync status` prints every entry, inspects peer dbs read-only (no WAL sidecars), and flags duplicate device_id, hostname mismatch, unpublished staging, and unimported peers ActivityWatch#684 Git-Session-Id: 375e1ec2-04d0-5884-9beb-cc5cd9c704c6
`aw-sync status` was a third directory walk beside list_remote_dbs (ActivityWatch#686) and collect_db_files (ActivityWatch#685). 3-level peers now come from the same list_remote_dbs + select_remote_dbs_by_device_id pair pull_all uses, so duplicate-device_id "not pulled" matches the pull path. Leftover 2-level / unrecognised entries sit on top of that list. ActivityWatch#687 Git-Session-Id: eca21d8a-f6da-5096-b28f-783778f4c09f
523f202 to
23e17f9
Compare
|
@ErikBjare Rebased cleanly onto upstream master now that #686 has merged. PR diff is now just the daemon layout changes — CI is retriggerring on the new head ( |
|
@TimeToBuildBob Green and mergeable on the new master. One thing to weigh before merging, though — this PR activates #692. Reproduced against the shipped That is exactly what a pull of an Android peer constructs, because aw-android sanitized the folder name but not the Nothing pulls today, which is why this is latent. This PR is what makes it reachable. Scope is aw-server-rust destinations only (aw-server-python has no such check), so: Android, and desktops that opt into the rust server — not the default desktop bundle. Suggest #692 (sanitize on import) lands with or before this, otherwise the fix for #682 hands Android users a hard failure in place of a silent one. #688 helps but is not sufficient alone — per-bucket non-fatal handling would skip the peer silently, which is better than dying but still loses the data. |
|
@TimeToBuildBob Second gate, and this one I would treat as harder than #692. Measured on a live sync folder: 58 of 63 peer databases are at Because The obvious mitigation is not available as-is: Not asking for that work in this PR. But it means the merge decision is Erik's judgement call between three options, and it is worth stating explicitly rather than discovering on release day:
|
|
@TimeToBuildBob Third and last item from me on this PR, and unlike the two gates this one is fixable inside the PR. Verified against the current head. This PR makes upgraded devices blind to not-yet-upgraded daemon peers. Before: the daemon read via After: Devices upgrade at different times. For the whole rollout window, every upgraded device loses sight of every peer still writing 2-level, and a peer that never upgrades stays invisible forever. That is #682's failure mode reintroduced in the other direction, with the same silence. Two things would close it, both small: (a) Keep reading root-level (b) On first run, if an own root-level db exists and no own 3-level db does, rename it into place ( Nothing should be deleted; the rename is the only write to existing files. With (a) and (b), the layout question stops needing a decision here: readers accept both depths as legacy detail, and the write layout can change once, later, together with any wire-format change. |
|
@TimeToBuildBob Parking this for v0.14.0 — not closing. Decision and reasoning on ActivityWatch/activitywatch#1445. Short version: the daemon switch is correct, but it targets the 3-level layout that the v2 format work retires, and every remaining item on it (legacy 2-level reading, first-run rename, then #692 and the #693 minimal slice to make the activated pulls safe) is scaffolding for that interim layout. When v2 lands the daemon should switch directly to One piece to split out and land now, since it helps the paths that pull today (Android, Stop work on the two in-PR additions. |
The default `aw-sync`/`aw-sync daemon` path drove `sync_run` against the
sync root, writing `{device_id}/test.db` and scanning two levels — so it
never saw `{hostname}/{device_id}/*.db` remotes from Android or
`aw-sync sync`. Route the default daemon through `sync_wrapper` like
the simple `sync` command, and only stage a local db on push.
Walker rewrite dropped: `pull_all` already uses `list_remote_dbs`
(3-level-only) after ActivityWatch#686. Broadening
`find_remotes` at the sync root would import the ActivityWatch#682 orphan.
Fixes ActivityWatch#682
Git-Session-Id: eca21d8a-f6da-5096-b28f-783778f4c09f
…grade
Host-layout pull was 3-level-only, so an upgraded daemon went blind to
peers still writing `{device_id}/*.db`. Fold those into list_remote_dbs
with hostname unknown. Same-device 2-level vs 3-level duplicates pick
newest data (mtime), not size, so a leftover cannot win forever.
On first push, if an own 2-level db exists and no 3-level dest does,
rename it (plus sqlite sidecars) into `{hostname}/{device_id}/test.db`
instead of creating an empty staging file and re-exporting history.
Nothing is deleted.
Git-Session-Id: 86fd4466-d56b-50f2-bff6-107033a71e12
23e17f9 to
96a5ca0
Compare
|
@ErikBjare Acknowledged. Parked, not closed. The two in-PR additions (legacy 2-level pull by newest mtime; first-run rename of own Split out as requested: #698 is the v0.14.0 remaining on the revised plan is #697 then the #693 minimal slice. |
* fix(aw-sync): do not stage a local db on pull-only sync_run
`sync_wrapper::pull` walks a peer host folder via `sync_run(..., Pull)`.
That used to call `setup_local_remote` unconditionally, creating
`{peer_host}/{our_device_id}/test.db` in a folder we do not own.
Only open a staging datastore when the pass actually pushes.
Split out of #685; the daemon layout switch
stays parked there.
Git-Session-Id: 86fd4466-d56b-50f2-bff6-107033a71e12
* fix(aw-sync): create sync root on pull-only without staging
Pull-only skipped setup_local_remote, so a missing sync root made
find_remotes NotFound and the advanced/daemon pull path exit instead
of warning on an empty dir. Create the root; still do not create
{root}/{our_device_id}/.
Git-Session-Id: 657af204-9f0a-5a4c-b756-89c0f521ef53
…t errors non-fatal Two issues block pulling peers whose bucket hostname contains whitespace (e.g. "POCO F8 Ultra" from Android devices before aw-android#272): 1. `get_or_create_sync_bucket` derived the destination ID from the raw hostname. On aw-server-rust destinations, `create_bucket` rejects new buckets with whitespace hostnames (ActivityWatch#658), so the first pull of such a peer returns HTTP 400, which propagated as `?` and aborted the whole sync pass (every subsequent peer also skipped). Fix: before creating a new bucket, sanitize whitespace → `_` in both the bucket ID and the hostname field. The legacy (unsanitized) ID is checked first so any existing pre-sanitization import is reused, avoiding the full re-import fork (#1373). 2. A single bucket failure with `?` in `sync_datastores` aborted the entire pass. All remaining peers were skipped. Blast radius: daemon returns Err → exits → supervisor restarts → same failure → budget exhausted → ActivityWatch#688. Fix: per-bucket errors in `sync_datastores` are now non-fatal (warn + continue). A broken or invalid bucket is skipped; healthy buckets still sync. Updated two tests that relied on the old fatal-error behavior. Fixes ActivityWatch#692. Related: ActivityWatch#658, ActivityWatch#685, ActivityWatch#688, ActivityWatch/aw-android#272. Git-Session-Id: c8bb
🤖 AI code reviewSafe to merge — 2 findings disposed (wontfix)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
This PR changes the default daemon sync path to use the host-layout (pull_all + push) instead of driving sync_run against the sync root, and makes setup_local_remote only run when the mode pushes. It also broadens list_remote_dbs to include 2-level leftover dbs, adds mtime-based selection for mixed layouts, and adds promote_legacy_own_db to rename leftover 2-level own dbs into the 3-level path. Tests are updated accordingly. Not safe to merge — 2 P1 openConfidence 2/5 3 findings · ❌ 2 P1 ·
|
|
Status note on the monitoring triggers that fired this morning (merge conflict + AI review), so they don't keep re-flagging:
|
…t errors non-fatal Two issues block pulling peers whose bucket hostname contains whitespace (e.g. "POCO F8 Ultra" from Android devices before aw-android#272): 1. `get_or_create_sync_bucket` derived the destination ID from the raw hostname. On aw-server-rust destinations, `create_bucket` rejects new buckets with whitespace hostnames (ActivityWatch#658), so the first pull of such a peer returns HTTP 400, which propagated as `?` and aborted the whole sync pass (every subsequent peer also skipped). Fix: before creating a new bucket, sanitize whitespace → `_` in both the bucket ID and the hostname field. The legacy (unsanitized) ID is checked first so any existing pre-sanitization import is reused, avoiding the full re-import fork (#1373). 2. A single bucket failure with `?` in `sync_datastores` aborted the entire pass. All remaining peers were skipped. Blast radius: daemon returns Err → exits → supervisor restarts → same failure → budget exhausted → ActivityWatch#688. Fix: per-bucket errors in `sync_datastores` are now non-fatal (warn + continue). A broken or invalid bucket is skipped; healthy buckets still sync. Updated two tests that relied on the old fatal-error behavior. Fixes ActivityWatch#692. Related: ActivityWatch#658, ActivityWatch#685, ActivityWatch#688, ActivityWatch/aw-android#272. Git-Session-Id: c8bb
The supervised 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 (ActivityWatch#688). Partial failure is now warn+continue. Total failure (every peer failed) is still Err so a down destination is not reported as success. Git-Session-Id: fcff5116-c5d0-5209-9804-33fc54c5b5a5 ActivityWatch#697 isolates host-layout pull_all; this is the daemon path (ActivityWatch#685 parked).
|
Converted to draft. DIRTY is still expected while this is parked for v0.14.0 (ActivityWatch/activitywatch#1445); a status comment does not suppress Did not rebase. Successor daemon switch happens fresh against the v2 layout. |
…t errors non-fatal Two issues block pulling peers whose bucket hostname contains whitespace (e.g. "POCO F8 Ultra" from Android devices before aw-android#272): 1. `get_or_create_sync_bucket` derived the destination ID from the raw hostname. On aw-server-rust destinations, `create_bucket` rejects new buckets with whitespace hostnames (ActivityWatch#658), so the first pull of such a peer returns HTTP 400, which propagated as `?` and aborted the whole sync pass (every subsequent peer also skipped). Fix: before creating a new bucket, sanitize whitespace → `_` in both the bucket ID and the hostname field. The legacy (unsanitized) ID is checked first so any existing pre-sanitization import is reused, avoiding the full re-import fork (#1373). 2. A single bucket failure with `?` in `sync_datastores` aborted the entire pass. All remaining peers were skipped. Blast radius: daemon returns Err → exits → supervisor restarts → same failure → budget exhausted → ActivityWatch#688. Fix: per-bucket errors in `sync_datastores` are now non-fatal (warn + continue). A broken or invalid bucket is skipped; healthy buckets still sync. Updated two tests that relied on the old fatal-error behavior. Fixes ActivityWatch#692. Related: ActivityWatch#658, ActivityWatch#685, ActivityWatch#688, ActivityWatch/aw-android#272. Git-Session-Id: c8bb
…t errors non-fatal (#697) * fix(aw-sync): sanitize whitespace hostnames on import; make per-bucket errors non-fatal Two issues block pulling peers whose bucket hostname contains whitespace (e.g. "POCO F8 Ultra" from Android devices before aw-android#272): 1. `get_or_create_sync_bucket` derived the destination ID from the raw hostname. On aw-server-rust destinations, `create_bucket` rejects new buckets with whitespace hostnames (#658), so the first pull of such a peer returns HTTP 400, which propagated as `?` and aborted the whole sync pass (every subsequent peer also skipped). Fix: before creating a new bucket, sanitize whitespace → `_` in both the bucket ID and the hostname field. The legacy (unsanitized) ID is checked first so any existing pre-sanitization import is reused, avoiding the full re-import fork (#1373). 2. A single bucket failure with `?` in `sync_datastores` aborted the entire pass. All remaining peers were skipped. Blast radius: daemon returns Err → exits → supervisor restarts → same failure → budget exhausted → #688. Fix: per-bucket errors in `sync_datastores` are now non-fatal (warn + continue). A broken or invalid bucket is skipped; healthy buckets still sync. Updated two tests that relied on the old fatal-error behavior. Fixes #692. Related: #658, #685, #688, ActivityWatch/aw-android#272. Git-Session-Id: c8bb * fix(aw-sync): match Android hostname sanitizer; isolate per-peer errors Whitespace-only replace ("POCO F8 Ultra" → "POCO_F8_Ultra") would fork the destination the day aw-android#272 migrates the phone's hostname column to poco_f8_ultra. Use Android's exact algorithm (lowercase, [^a-z0-9_-]+ → _, trim _) via a shared sanitize_hostname() that must stay byte-identical to DeviceHostname.kt. Also: - Trigger sanitization when the source hostname has whitespace even if the derived ID is already clean ($aw.sync.origin can be sanitized while bucket.hostname is not; create_bucket would 400 on the field). - pull_all: per-peer warn+continue so a failed open does not abort the pass and skip every peer after it (#688 / #693). Git-Session-Id: 2097767 * fix(aw-sync): refuse pull when hostname sanitizes to unknown Android maps empty/punctuation-only names to the "unknown" sentinel. A hostname like " * " contains whitespace so the sanitizer runs, then becomes "unknown", and get_or_create would create -synced-from-unknown — mixing every such remote into one destination. The hostname=="unknown" guard in sync_datastores never sees this because the source hostname is not yet the sentinel. Refuse on pull; per-bucket warn+continue skips the junk bucket and a healthy sibling still syncs. Git-Session-Id: 2679931 * fix(aw-sync): sanitize hostnames unconditionally; fail closed on total error Always run DeviceHostname.kt sanitization, not only on whitespace, so PIXEL8 and erb-m2.localdomain land on the same IDs Android's migration will use. Lookup stays raw then sanitized; create under the sanitized ID when it differs. sync_datastores and pull_all still skip individual buckets/peers, but return Err when every attempt fails so a down destination is not reported as success. Git-Session-Id: 82e7d9e5-0c8a-58c0-b129-a46f96ef6953 * fix(aw-sync): warn that a skipped bucket may be partially written A failed sync_one can leave dest with a partial chunk. The per-bucket warn+continue now names that, so a skipped bucket is not mistaken for an untouched skip. Git-Session-Id: pm-697-rebase-2026-09-16
|
Reminder for any session picking this up from #682: parked for v0.14.0 by decision — see ActivityWatch/activitywatch#1445. Do not rebase or bring it to green; the daemon moves directly to the v2 layout. |
96a5ca0 to
b1b1b96
Compare
b1b1b96 to
96a5ca0
Compare
|
Correction — the rebase announced in my 12:27 comment should not have happened, and I've reverted it. @ErikBjare's 12:22 reminder landed while that rebase was already in flight. The session didn't re-read the thread before posting, so it pushed Restored the branch to its parked head |
|
Closing as superseded — each part has landed or has a better home:
Residual to keep in mind until v2: the daemon still writes its own staging at the 2-level root ( Thanks for the work here, @TimeToBuildBob — the analysis in this PR is what made the narrow fix obvious. |
Fixes #682.
Stacked on #686 (
RemoteDb/list_remote_dbs). Merge #686 first.Problem
aw-sync(no args) andaw-sync daemondrovesync_runagainst the sync-folder root:{sync_dir}/{device_id}/test.db(2-level)find_remotes()walked two levels, so{hostname}/{device_id}/*.dbfrom Android /aw-sync syncwas invisibleNet effect: the default daemon is push-only into a directory nothing else reads, and it silently never pulls.
Same path, second bug:
sync_wrapper::pullcallssync_run(..., Pull)on a peer host folder, andsetup_local_remotestill created{peer_host}/{our_device_id}/test.db— writing into a folder we don't own.Fix
--start-date/--buckets/--sync-db) now doessync_wrapper::pull_all+pushper cycle, same 3-level layout asaw-sync syncand Android.setup_local_remoteonly runs when the mode actually pushes (Option<Datastore>).The parallel
find_remotes/get_remoteswalker rewrite from the first revision is dropped. After #686,pull_alluseslist_remote_dbs, which is 3-level-only — leftover{device_id}/test.dbat the sync root (the #682 orphan) is not a pull candidate. That is intentional; a test on #686 locks it. The advanced--buckets/--start-date/--sync-dbpath still usesfind_remotes(2-level relative to the given directory).Not in this PR: migrating existing 2-level
{device_id}/test.dbfiles (the 1.19GB root db on erb-m2). After this lands, new daemon pushes go where peers can read them; the leftover root db still needs a one-time move/cleanup.Tests
cargo test -p aw-sync— includinglist_remote_dbs_skips_legacy_two_level_root_dbsfrom #686.