Read and write fewer times per sync #51

Merged
tiagoagueda merged 3 commits from 0.6.0-fixes into main 2026-11-12 02:07:11 +00:00
Owner

The plugin's three 0.6.0 performance issues, one commit each.

  • Closes #46: a write answered without an ETag asks for that one resource (a Depth 0 PROPFIND) instead of listing the whole collection again (3 contacts and 1 interview: 4 and 2 listings down to 1 each).
  • Closes #45: cards and events are read with addressbook-multiget and calendar-multiget REPORTs, 50 at a time, instead of a GET each (first run over 120 cards: 120 GETs down to 3 REPORTs); a refused report, a 404 or a resource too large falls back to a GET. Sync tokens stay deferred, as decided.
  • Closes #47: contacts are read with their numbers and links prefetched, an unchanged contact is skipped before its savepoint, and each half checks its links' records in one query (per idle run: 4 queries per unchanged contact down to 2 on the current core pin, 0 once the pin passes core #1171; 1 per old interview down to 0).

Tested: 108 pass on the pinned core, and on the core's main too; each new test fails without its fix. No new strings.

🤖 Generated with Claude Code

The plugin's three 0.6.0 performance issues, one commit each. - Closes #46: a write answered without an ETag asks for that one resource (a Depth 0 PROPFIND) instead of listing the whole collection again (3 contacts and 1 interview: 4 and 2 listings down to 1 each). - Closes #45: cards and events are read with `addressbook-multiget` and `calendar-multiget` REPORTs, 50 at a time, instead of a GET each (first run over 120 cards: 120 GETs down to 3 REPORTs); a refused report, a 404 or a resource too large falls back to a GET. Sync tokens stay deferred, as decided. - Closes #47: contacts are read with their numbers and links prefetched, an unchanged contact is skipped before its savepoint, and each half checks its links' records in one query (per idle run: 4 queries per unchanged contact down to 2 on the current core pin, 0 once the pin passes core #1171; 1 per old interview down to 0). Tested: 108 pass on the pinned core, and on the core's `main` too; each new test fails without its fix. No new strings. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A server that stores something other than the bytes it was sent must not
answer a PUT with an ETag (RFC 4791 §5.3.4, RFC 6352 §6.3.2.3), and
SabreDAV (Nextcloud, Baïkal) drops it whenever it repairs a body, as it
does for the METHOD line of an event's first try. The fallback then
listed the whole collection with a Depth 1 PROPFIND to pick out one
ETag, so pushing N records cost N listings of a collection growing by
one each time: about 45,000 listing entries for a first push of 300
contacts.

Server.put now asks for the written resource alone, a Depth 0 PROPFIND
of its href read with the same href checks as a listing. Pushing three
contacts and an interview to such a server went from four listings of
the address book and two of the calendar to one of each, plus one
Depth 0 PROPFIND per write. The fake server learns to answer a PUT
without an ETag and a Depth 0 PROPFIND of a resource.

Closes #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every card no link claimed and every twin whose ETag moved was read with
a GET of its own, and so was every unclaimed event whenever an
interview had no twin. In read-only mode nothing is pushed to settle
that, so every event on the calendar that is not Postulo's was fetched
again on every run: 500 GETs an hour on a shared calendar of 500 events.

Server.multiget asks for them with addressbook-multiget (RFC 6352 §8.7)
and calendar-multiget (RFC 4791 §7.9), which every CardDAV and CalDAV
server must answer, fifty addresses per REPORT, and both halves of the
sync fetch what they will read in one call before their loops. An
answer over its limit (4 MB) is asked again in two halves; only the
addresses asked for are taken from the answer; a resource the server
leaves out, one over the resource limit, or a report the server refuses
is read with get() as before, which says why it was skipped. Read-only
mode is unchanged: it still reads the foreign events on every run, now
as a REPORT per fifty (skipping the unchanged ones needs sync tokens,
left for later). The requests are written by hand beside the existing
PROPFIND bodies: the plugin depends on no DAV library, by design (#13,
#33).

Measured against the test double: a first run over 120 cards made 120
GETs and makes 3 REPORTs (50, 50, 20); a read-only run over 60 foreign
events made 60 GETs a run and makes 2 REPORTs; a card changed on the
phone is read by one REPORT instead of a GET. The fake server learns
the two multiget reports, with a switch to refuse them.

Closes #45

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop asking the database once per contact and old interview
Some checks failed
CI / test (pull_request) Failing after 2m36s
338722a7c1
An idle run still paid for every record it walked: a savepoint pair and
two queries for the primary number and link of each contact, and one
exists() per link whose contact was not visible or whose interview had
fallen out of the 90-day window. That last loop grows with the person's
whole interview history and runs every interval.

The contacts are read with their numbers and links prefetched; with a
core that reads the primary from prefetched rows (postulo/postulo#1171,
on the core's main since 2026-11-10, in no release yet) that is two
queries a run instead of two per contact, and with the pinned core
v0.5.0 the answers are the same and the queries as before. A contact
whose twin's ETag and card digest are both as last synced is now passed
over before Run.each opens a savepoint for it, and the existence of the
missing records is asked once for all of them in each half.

Counted in the tests, per idle run: each unchanged contact cost 4
queries and now costs 2 on the pinned core and 0 on the core's main;
each old interview cost 1 query and now costs none.

Closes #47

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed 0.6.0-fixes from 338722a7c1
Some checks failed
CI / test (pull_request) Failing after 2m36s
to 01a70108ab
All checks were successful
CI / test (pull_request) Successful in 4m13s
2026-11-12 01:45:30 +00:00
Compare
tiagoagueda deleted branch 0.6.0-fixes 2026-11-12 02:07:11 +00:00
Sign in to join this conversation.
No reviewers
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!51
No description provided.