Read only: a conflict that ours wins is recorded again on every run, growing contact notes and the timeline #21

Closed
opened 2026-10-01 10:47:52 +00:00 by tiagoagueda · 1 comment
Owner

Found on 2026-09-29 in a read-only review of postulo-dav at 97abc9779, reading the conflict branches of the sync and running them against the test suite's fake server.

When a record changed on both sides and the local change is the later one, the sync writes the losing remote version down (a note on the contact, an entry on the application's timeline) and sets the link's ETag to the remote one only in memory, counting on the push that follows to save the link. In read-only mode that push is skipped before anything is saved, so the next run sees the same remote change as new and writes the same note again. It does so on every run, for as long as read-only stays on.

  • Where: src/postulo_dav/sync.py:472-484 (_sync_contacts: appends "Changed on the phone on …" to contact.notes, saves the contact, then link.etag = resource.etag without saving the link); src/postulo_dav/sync.py:498-501 (if read_only: report.skipped += 1; continue); src/postulo_dav/sync.py:611-618 (_sync_interviews: record_event(... "Changed on the phone, but changed here later; kept ours."), then link.etag = resource.etag); src/postulo_dav/sync.py:637-640 (the same read-only skip).
  • What happens:
    1. A connection is set to Read only. A contact is edited on the phone and then, later, in Postulo.
    2. The next run finds both changed, ours later: it appends the phone's card to the contact's notes and saves the contact, which also moves updated_at to now.
    3. The push is skipped; the link still holds the old ETag.
    4. Every later run repeats step 2, and because updated_at was just moved, ours always wins again.
      Reproduced: three read-only runs left three "Changed on the phone on" paragraphs in the contact's notes, with the link's ETag still "v1"; four runs on an interview left four "kept ours" entries on the timeline. Each run reports only "1 skipped". The same repetition follows outside read-only mode whenever the push after an ours-wins decision fails: a DavError (for example a server refusing the PUT) propagates after the note is saved, so the next run records it again.
  • Why it matters: the contact's notes and the application's timeline fill with copies of the same text at every sync interval (as often as every 15 minutes), and the only way out is to turn read-only off.
  • Fix: once the losing remote version has been recorded, store that remote ETag at once (link.etag = resource.etag; link.save(update_fields=["etag", "updated_at"])) in both branches, before deciding whether to push. A skipped or failed push then leaves the local change pending without recording the same conflict again.

Done when

  • Tests in tests/test_dav.py, one for a contact and one for an interview, that create a both-sides change with ours later on a read-only connection, sync three times, and assert exactly one note paragraph and exactly one "kept ours" timeline entry.
Found on 2026-09-29 in a read-only review of postulo-dav at 97abc9779, reading the conflict branches of the sync and running them against the test suite's fake server. When a record changed on both sides and the local change is the later one, the sync writes the losing remote version down (a note on the contact, an entry on the application's timeline) and sets the link's ETag to the remote one only in memory, counting on the push that follows to save the link. In read-only mode that push is skipped before anything is saved, so the next run sees the same remote change as new and writes the same note again. It does so on every run, for as long as read-only stays on. - **Where:** `src/postulo_dav/sync.py:472-484` (`_sync_contacts`: appends "Changed on the phone on …" to `contact.notes`, saves the contact, then `link.etag = resource.etag` without saving the link); `src/postulo_dav/sync.py:498-501` (`if read_only: report.skipped += 1; continue`); `src/postulo_dav/sync.py:611-618` (`_sync_interviews`: `record_event(... "Changed on the phone, but changed here later; kept ours.")`, then `link.etag = resource.etag`); `src/postulo_dav/sync.py:637-640` (the same read-only skip). - **What happens:** 1. A connection is set to *Read only*. A contact is edited on the phone and then, later, in Postulo. 2. The next run finds both changed, ours later: it appends the phone's card to the contact's notes and saves the contact, which also moves `updated_at` to now. 3. The push is skipped; the link still holds the old ETag. 4. Every later run repeats step 2, and because `updated_at` was just moved, ours always wins again. Reproduced: three read-only runs left three "Changed on the phone on" paragraphs in the contact's notes, with the link's ETag still `"v1"`; four runs on an interview left four "kept ours" entries on the timeline. Each run reports only "1 skipped". The same repetition follows outside read-only mode whenever the push after an ours-wins decision fails: a `DavError` (for example a server refusing the PUT) propagates after the note is saved, so the next run records it again. - **Why it matters:** the contact's notes and the application's timeline fill with copies of the same text at every sync interval (as often as every 15 minutes), and the only way out is to turn read-only off. - **Fix:** once the losing remote version has been recorded, store that remote ETag at once (`link.etag = resource.etag; link.save(update_fields=["etag", "updated_at"])`) in both branches, before deciding whether to push. A skipped or failed push then leaves the local change pending without recording the same conflict again. ## Done when - Tests in `tests/test_dav.py`, one for a contact and one for an interview, that create a both-sides change with ours later on a read-only connection, sync three times, and assert exactly one note paragraph and exactly one "kept ours" timeline entry.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:52 +00:00
Author
Owner

Fixed by pull request #35, merged as fb8bbd7ae.

Fixed by pull request #35, merged as fb8bbd7ae.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-dav#21
No description provided.