Skip to content

aw-sync: no stored cursor — resume-from-destination silently never syncs late-arriving events #696

Description

@ErikBjare

sync_one has no stored cursor. It infers where to resume by reading the newest event already in the destination:

let most_recent_events = ds_to.get_events(bucket_to.id.as_str(), None, None, Some(1))?;
let resume_sync_at = most_recent_events
    .first()
    .map(|e| e.timestamp + e.duration)
    .or(sync_spec.start);

then fetches only events newer than that. The destination copy is being used as its own progress marker.

Consequence 1: events that arrive late never sync, silently

Events are not always appended in timestamp order. An importer (aw-import-*, screentime imports), a retroactively edited stopwatch entry, or any backfill writes events with timestamps older than the bucket's current newest. Those events are permanently invisible to sync: the cursor is already past them, and nothing ever looks back. No error, no warning — the pass reports ✓ Already up to date!.

Consequence 2: it is the mechanism behind #683

Because the cursor lives in the destination rather than being per-source, two folders for one device write into the same destination bucket and the first one to be imported sets the resume point for the second. That is how a 4,068-event partial slice silently suppressed 1,027,343 events of real history. #686 fixes the trigger (duplicate folders) but the amplifier is this cursor design.

Consequence 3: it forces the fragile pagination

Because the cursor is a timestamp, sync_one has to reason carefully about events sharing an identical timestamp at page boundaries — the boundary_ts tie-handling loop, with a documented pathological case it explicitly declines to handle. A monotonic source-side cursor removes that reasoning entirely.

Proposal

Store an explicit cursor per (source_device_id, source_bucket_id) on the destination, keyed by the source's events.id rowid, which is monotonic in insertion order:

sync_cursor(source_device_id, source_bucket_id, last_source_rowid, updated)
  • Fetch WHERE id > last_source_rowid ORDER BY id LIMIT n — no timestamp ties, no boundary loop, and late-arriving events are picked up because they get a new rowid even though their timestamp is old.
  • Initialise from today's resume-from-newest so existing installs migrate with no re-import.
  • Per-source keying means two folders for one device cannot contaminate each other's progress.

Caveat worth designing around: rowids are only monotonic within one source database. If a peer's staging db is rebuilt from scratch its rowids restart, so the cursor needs to be invalidated when the source's identity or generation changes — which is another argument for per-device metadata in the folder (ActivityWatch/activitywatch#302, #691). A cheap interim guard is to store the source's event count alongside the cursor and reset if it goes backwards.

Credit: found in a design review of aw-sync.

Related: #683, #686, #691, ActivityWatch/activitywatch#302.

cc @TimeToBuildBob

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions