A catch-up cursor cannot walk past rows that share one updated_at #245
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Postulo/postulo#245
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#230 gave every list an
updated_sincecursor: ask for what changed at or after a moment,oldest change first, take the
updated_atof the last row you read and ask again fromthere. It works until several rows carry the same
updated_at, and then it stops dead.The filter is
updated_at__gte=since. A client whose last row is one of a group sharing atimestamp asks again from that timestamp and is handed the same group again. Where the group
is larger than
limit, every page after that is rows it has already seen, for ever: theclient either loops or, if it stops when a page brings nothing new, silently never reads the
rest of the account. Nothing raises, nothing logs, and the copy it is keeping is quietly
short.
This is not a rare shape.
updated_atisauto_now, so a bulk edit, an import, a migrationbackfill or a spreadsheet import all write a run of rows within the same microsecond — and
the faster the machine, the longer the run. It surfaced as
tests/test_api_lists.py::test_catching_up_reads_forward_so_a_cursor_can_advancefailing onan idle machine while passing on a loaded one, which is the same fault wearing a disguise:
twelve rows created in a tight loop landed four-deep on one timestamp, and a four-row page
could not get past them.
Proposal
Make the cursor a keyset rather than a timestamp: alongside
updated_since, an optionalafter_id, and the filterwhich is exactly the order the list is already sorted in (
updated_at,pk), so it names aposition rather than a moment. Without
after_idthe behaviour is what it is today, sonothing that works now breaks.
updated_at; it carriesidtoo, so a client has both halvesand needs no new field.
updated_atandid".Found while landing #224 on top of #230, 2026-09-16.