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
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
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#1154
Loading…
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?
What exists today
core/views_errands.py:26fixesPOLL_SECONDS = 2;templates/core/partials/errand_state.html:24-29putshx-trigger="every {{ poll_seconds }}s"on every unfinished state and swaps the fragment whole. The same fragment is included bytemplates/server/backups.html:32. Polling stops only when the errand finishes. An errand whose worker was killed stays waiting or working untilcore/errands.py:193-208fail_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 fireshtmx:afterSwap, andstatic/js/app.js:32-39onContentReadyregisters 24 page-widereadyfunctions 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
state_context()computespoll_secondsfrom 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.fail_interruptedis 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.readyfunctions that only concern table, board or form markup checkevent.targetand 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 minuteAccessibility
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 bytests/test_errands.py::test_the_live_region_is_not_the_element_the_poll_replaces,::test_an_unfinished_page_polls_and_a_finished_one_stops, andtests/e2e/test_logo_dialog.py(result focused at line 124, text at 185)Decisions
onContentReadyscoping (a separate front-end issue, measured on the board first).Found by the efficiency and reliability audit of 2026-10-30 (finding A8; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).