Errands: the state fragment is polled every two seconds for as long as the errand is unfinished, up to a day if the worker died #1154

Closed
opened 2026-10-31 03:04:23 +00:00 by tiagoagueda · 0 comments
Owner

What exists today

core/views_errands.py:26 fixes POLL_SECONDS = 2; templates/core/partials/errand_state.html:24-29 puts hx-trigger="every {{ poll_seconds }}s" on every unfinished state and swaps the fragment whole. The same fragment is included by templates/server/backups.html:32. Polling stops only when the errand finishes. An errand whose worker was killed stays waiting or working until core/errands.py:193-208 fail_interrupted(hours=24) marks it failed, and that runs only from the scheduler (notifications/management/commands/send_due_reminders.py:214); an instance with a worker but no scheduler keeps it unfinished, and the page polls for as long as it is open.

Each poll is a signed-in request: session, authentication, an ownership-checked query, and, under SQLite, a BEGIN IMMEDIATE (filed as R1, which takes this view out of the transaction). Each swap fires htmx:afterSwap, and static/js/app.js:32-39 onContentReady registers 24 page-wide ready functions on that event (25 calls, one the definition), so every two seconds each of them rescans the document for the one fragment that changed.

What would change

  • A back-off decided by the server, with no change to the script: state_context() computes poll_seconds from the errand's age, for example 2 s for the first 20 seconds, 5 s up to two minutes, then 15 s, capped at 30 s. The fragment already carries the interval, so each poll sets the next one.
  • A ceiling the page respects without waiting for the scheduler: past the handler's own reasonable deadline, the poll answers with the failed wording (or fail_interrupted is also called for that one errand from the poll view itself, with a deadline of its own per kind), so the page stops promising and stops polling.
  • Optionally, the ready functions that only concern table, board or form markup check event.target and return early for a swap that cannot contain their markup; that helps every swap, not only this one, and can be a separate PR.

What it saves or protects

medium: a tab left on an errand whose worker died sends 1,800 requests an hour, 43,200 before fail_interrupted (24 h) ends it, and never stops on an instance with no scheduler; a back-off to 30 s cuts that by about 15 times after the first minute

Accessibility

must be checked: the change must still be announced by the page's role="status" region (templates/core/errand.html:18), focus must still go to the result in the logo dialog (data-focus-result), and the <noscript> Check again link stays. Guarded by tests/test_errands.py::test_the_live_region_is_not_the_element_the_poll_replaces, ::test_an_unfinished_page_polls_and_a_finished_one_stops, and tests/e2e/test_logo_dialog.py (result focused at line 124, text at 185)

Decisions

  • Split? Recommended: split into the back-off and per-errand deadline (this issue) and the onContentReady scoping (a separate front-end issue, measured on the board first).
  • Back-off steps? Recommended: 2 s, 5 s, 15 s, 30 s, by the errand's age; the first seconds, when most errands finish, stay as quick as today.

Found by the efficiency and reliability audit of 2026-10-30 (finding A8; index postulo/postulo#1108). Checked against main at daee30b15 (2026-10-30).

## What exists today `core/views_errands.py:26` fixes `POLL_SECONDS = 2`; `templates/core/partials/errand_state.html:24-29` puts `hx-trigger="every {{ poll_seconds }}s"` on every unfinished state and swaps the fragment whole. The same fragment is included by `templates/server/backups.html:32`. Polling stops only when the errand finishes. An errand whose worker was killed stays *waiting* or *working* until `core/errands.py:193-208` `fail_interrupted(hours=24)` marks it failed, and that runs only from the scheduler (`notifications/management/commands/send_due_reminders.py:214`); an instance with a worker but no scheduler keeps it unfinished, and the page polls for as long as it is open. Each poll is a signed-in request: session, authentication, an ownership-checked query, and, under SQLite, a `BEGIN IMMEDIATE` (filed as R1, which takes this view out of the transaction). Each swap fires `htmx:afterSwap`, and `static/js/app.js:32-39` `onContentReady` registers 24 page-wide `ready` functions on that event (25 calls, one the definition), so every two seconds each of them rescans the document for the one fragment that changed. ## What would change - A back-off decided by the server, with no change to the script: `state_context()` computes `poll_seconds` from the errand's age, for example 2 s for the first 20 seconds, 5 s up to two minutes, then 15 s, capped at 30 s. The fragment already carries the interval, so each poll sets the next one. - A ceiling the page respects without waiting for the scheduler: past the handler's own reasonable deadline, the poll answers with the failed wording (or `fail_interrupted` is also called for that one errand from the poll view itself, with a deadline of its own per kind), so the page stops promising and stops polling. - Optionally, the `ready` functions that only concern table, board or form markup check `event.target` and return early for a swap that cannot contain their markup; that helps every swap, not only this one, and can be a separate PR. ## What it saves or protects medium: a tab left on an errand whose worker died sends 1,800 requests an hour, 43,200 before `fail_interrupted` (24 h) ends it, and never stops on an instance with no scheduler; a back-off to 30 s cuts that by about 15 times after the first minute ## Accessibility must be checked: the change must still be announced by the page's `role="status"` region (`templates/core/errand.html:18`), focus must still go to the result in the logo dialog (`data-focus-result`), and the `<noscript>` *Check again* link stays. Guarded by `tests/test_errands.py::test_the_live_region_is_not_the_element_the_poll_replaces`, `::test_an_unfinished_page_polls_and_a_finished_one_stops`, and `tests/e2e/test_logo_dialog.py` (result focused at line 124, text at 185) ## Decisions - **Split?** Recommended: split into the back-off and per-errand deadline (this issue) and the `onContentReady` scoping (a separate front-end issue, measured on the board first). - **Back-off steps?** Recommended: 2 s, 5 s, 15 s, 30 s, by the errand's age; the first seconds, when most errands finish, stay as quick as today. --- Found by the efficiency and reliability audit of 2026-10-30 (finding A8; index postulo/postulo#1108). Checked against `main` at `daee30b15` (2026-10-30).
tiagoagueda added this to the 0.6.0 milestone 2026-10-31 03:04:23 +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#1154
No description provided.