Notifications: without a worker, status, interview and offer messages are sent inside the caller's transaction #578
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#578
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?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, followingslow.tellfrom the application services to the notifiers.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atcore/csv_import.py:941(the import'stransaction.atomic()); the other locations have not moved.slow.tellhands the message toerrands.send, which, on an instance without a worker (POSTULO_BACKGROUND_WORKis off by default), performs the errand on the spot. Every caller oftellis inside a transaction, so each notifier's network call (SMTP, Apprise, Web Push) runs while the transaction is open and before it commits. #220 and #247 took slow work out of request transactions; this path puts it back.notifications/slow.py:139-149(tell);core/errands.py:97-101(send, which callsperforminline whenworker_expected()is false);applications/services.py:87-88,149and194(change_status:@transaction.atomic, aselect_for_updateon the application, thenslow.tell);applications/services.py:425,461,696,713(schedule_interview,reschedule_interview,record_offer,revise_offer, all atomic);core/csv_import.py:946(the whole import in onetransaction.atomic(), callingchange_statusper row), reached fromcore/views_import.py, which also runs underATOMIC_REQUESTS.sendonly writes a row).change_statusraises (a later row of the import, the rest of the view), the transaction rolls back, but the messages about the changes that never happened have already been sent.tell, queue after commit:transaction.on_commit(lambda: errands.send("notify", owner, ...)), with the payload built before the lambda. The inline errand then runs outside the transaction and only for committed changes, and a queued one is guaranteed to find its rows.on_commitruns immediately when no transaction is open, so callers outside one are unaffected.Related to #220 and #247, which moved slow work out of request transactions but did not cover this path.
Done when
tests/test_webhooks.py(or a newtests/test_notify_after_commit.py) with a recording notifier that hasstatus_changedon:change_statusinsidetransaction.atomic()that then raises sends nothing, and one that commits sends once, with the send observed only after commit (django_capture_on_commit_callbacks(execute=True)).