Document stores: *Send now* re-claims a copy the scheduler is already sending, so it is put twice #509

Closed
opened 2026-10-01 10:47:11 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the claim that #221 added to store copies, and confirmed by running claim against the snapshot.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

claim() is a conditional update against the next_attempt_at the caller read, and a claim moves next_attempt_at a quarter of an hour ahead. The scheduler never sees a claimed copy again, because it only reads copies that are due. Send now reads every copy not yet sent, whatever its next_attempt_at. If it reads after the scheduler has claimed a copy, it reads the lease value, its own claim matches that value, and it sends the same copy again while the first put is still under way.

  • Where: documents/archiving.py:192-208, claim; documents/archiving.py:224-242, send_now, whose query is DocumentCopy.objects.filter(**_lookup(document)).exclude(status=CopyStatus.SENT); the caller is documents/views.py:678-728, SendCopiesNowView.
  • What happens: a new upload or render schedules its copies as due at once. The scheduler's next pass claims one and starts uploading it to Paperless or WebDAV. The person, seeing the document still marked pending, presses Send now. send_now reads the copy with its leased next_attempt_at, claim succeeds against that value, and send_copy calls plugin.put a second time. Run against the snapshot: after a scheduler-style pending_copies() and claim(), a fresh read followed by claim() returned True, and so did a third. Only a caller holding the value read before the first claim loses. The same holds for two Send now requests for one document from two tabs, or with scripts off; the once-only submit in static/js/app.js (the "a form is sent once" block) stops a double click in one page.
  • Why it matters: the store receives the document twice, or rejects the second as a duplicate and the copy is recorded as failed although it arrived. #221 was closed as fixing "a pass overlapping the Send now somebody just pressed", and claim's docstring and the wiki still say nothing is ever sent twice.
  • Fix: keep the lease apart from the retry time. Add a claimed_until column (or a sending status with a lease timestamp); have claim update only where claimed_until IS NULL OR claimed_until < now, in the same statement; clear it in send_copy when the outcome is recorded. send_now can then go on overriding the retry wait of a failed copy without ever taking a live lease, and a copy whose worker died still comes back when the lease runs out.

Done when

  • A test in tests/test_stores.py claims a copy as the scheduler does, then calls send_now for its document with a fake store, and asserts the store's put was called once in total and send_now reported nothing sent.

Related to #221, which added the claim and was closed as covering this overlap.

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the claim that #221 added to store copies, and confirmed by running `claim` against the snapshot. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. `claim()` is a conditional update against the `next_attempt_at` the caller read, and a claim moves `next_attempt_at` a quarter of an hour ahead. The scheduler never sees a claimed copy again, because it only reads copies that are due. *Send now* reads every copy not yet sent, whatever its `next_attempt_at`. If it reads after the scheduler has claimed a copy, it reads the lease value, its own claim matches that value, and it sends the same copy again while the first `put` is still under way. - **Where:** `documents/archiving.py:192-208`, `claim`; `documents/archiving.py:224-242`, `send_now`, whose query is `DocumentCopy.objects.filter(**_lookup(document)).exclude(status=CopyStatus.SENT)`; the caller is `documents/views.py:678-728`, `SendCopiesNowView`. - **What happens:** a new upload or render schedules its copies as due at once. The scheduler's next pass claims one and starts uploading it to Paperless or WebDAV. The person, seeing the document still marked pending, presses *Send now*. `send_now` reads the copy with its leased `next_attempt_at`, `claim` succeeds against that value, and `send_copy` calls `plugin.put` a second time. Run against the snapshot: after a scheduler-style `pending_copies()` and `claim()`, a fresh read followed by `claim()` returned `True`, and so did a third. Only a caller holding the value read before the first claim loses. The same holds for two *Send now* requests for one document from two tabs, or with scripts off; the once-only submit in `static/js/app.js` (the "a form is sent once" block) stops a double click in one page. - **Why it matters:** the store receives the document twice, or rejects the second as a duplicate and the copy is recorded as failed although it arrived. #221 was closed as fixing "a pass overlapping the *Send now* somebody just pressed", and `claim`'s docstring and the wiki still say nothing is ever sent twice. - **Fix:** keep the lease apart from the retry time. Add a `claimed_until` column (or a *sending* status with a lease timestamp); have `claim` update only where `claimed_until IS NULL OR claimed_until < now`, in the same statement; clear it in `send_copy` when the outcome is recorded. `send_now` can then go on overriding the retry wait of a failed copy without ever taking a live lease, and a copy whose worker died still comes back when the lease runs out. ## Done when - A test in `tests/test_stores.py` claims a copy as the scheduler does, then calls `send_now` for its document with a fake store, and asserts the store's `put` was called once in total and `send_now` reported nothing sent. Related to #221, which added the claim and was closed as covering this overlap.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:11 +00:00
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#509
No description provided.