Notifications: a send that fails at a notifier is lost, while a webhook delivery is retried for ten hours #1163
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#1163
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,
src/postulo/notifications/service.py:77-118_deliver: each notifier'ssend()is called once.ConnectionUnusableretires the connection (lines 94-105); any other exception is logged and written on the connection withrecord_test(False, …)(lines 106-111), and the notification is gone. Callers do not retry either:send_due_reminders.py:61-63stampsnotified_atwith a conditionalUPDATEbefore announcing, so a reminder whose only notifier failed is never announced again;applications/quiet.py:113andjobs/closing.py:116claim the same way.The webhook notifier is the exception: it only queues a row (
delivers_later), andnotifications/webhooks.py:35-36, 122-140retries it with a doubling delay from five minutes, eight attempts (about ten hours).Notification.key(send_due_reminders.py:93-97) is documented as stable "however many times one of them retries (#229)", so the payload is already shaped for retries; only the webhook uses that.postulo-apprise on the forge's
main(0cd0aa1),src/postulo_apprise/notifier.py:520-530send: raises whenapprise.notifyreturns false, which Apprise does when any one of the connection's services refused. So the core sees one failure per connection, not per service.The scout's second point, "URLs re-parsed per send" (
notifier.py:444-487,_problemsthenbuild): each send parses the field about three times and resolves each server's host forcheck_destination. That is deliberate, since the instance's destination policy and the name's addresses may have changed since the form was saved (postulo-apprise#10, #12, #15), and it costs milliseconds per send against sends that happen a few times a day. Refuted as a cost; not filed.What would change
Core: a failed
send()(an ordinary exception, notConnectionUnusable) queues a retry row for that connection and notification, reusing the webhook's schedule and claim logic (FIRST_RETRY, doubling,MAX_ATTEMPTS, the conditionalUPDATElease), delivered by the scheduler pass that already delivers webhooks. The row keeps the notification fields the webhook row keeps today, so it falls under the same export, erasure and pruning rules that PST-023 put on webhook rows. Retrying stops when the connection is switched off or retired, as for webhooks (#243).record_test(False)stays, so the connection still shows the failure at once.Generalising the webhook outbox into one "pending delivery" table for every notifier is the cleaner shape; a second table beside it is the smaller change. Either way no library is needed;
django_tasks(already in use) could carry the delay, but the webhook path already chose rows plus the scheduler, and one mechanism is easier to reason about.A side benefit: notifications sent from a request (
jobs/sharing.py:342) wait today for a slow notifier (Apprise's own deadline is 45 s,notifier.py:71); with a queue, the request could queue and return. That is a separate decision and not part of this issue.What it saves or protects
medium (reliability): a reminder announced while Telegram, ntfy, the mail server or a push service is briefly unreachable still arrives, minutes later, instead of never
Accessibility
none
Decided by the maintainer (2026-10-31)
retry failed notifier sends in core on the webhook schedule; duplicates on partial Apprise failure accepted and documented
Decisions
Found by the efficiency and reliability audit of 2026-10-30 (finding PR8; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).