Sync connections: a run that fails is tried again every interval for ever, and a sync cannot retire its connection #1162
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#1162
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/plugins/syncing.py:is_due(lines 70-75) looks only atsynced_atand the interval (15, 60, 240 or 1440 minutes, default 60, lines 35-41). It does not readlast_errororlast_ok_at._run_connection(lines 117-146) recordsreport.errorinlast_error, stampssynced_at, and the connection is due again one interval later. Nothing counts consecutive failures.SecretsUnreadableand anyException, but notConnectionUnusable(plugins/base.py:772-792). That exception, which switches a connection off with a sentence for the person, is honoured for stores (documents/archiving.py:177) and notifiers (notifications/service.py:94), not for syncs: a sync plugin that raised it would be logged with a traceback as a crash and dialled again next interval.postulo-imap,
src/postulo_imap/mailbox.py:582-589(connect): a refusedLOGINbecomesMailboxError("Sign-in refused…"), whichsync.py:253-255returns asSyncReport(error=…). So after a password change, every interval opens DNS, TCP, TLS and a failedLOGINagainst the person's mail server, until the person notices the error on the connection. The same holds for postulo-dav and postulo-francetravail, which go through the same scheduler.The lockout risk the scout named is real but smaller than stated: a lockout policy that counts failures in a short window (fail2ban's default 5 in 10 minutes, Active Directory's usual 30-minute observation window) is not reached by one attempt every 15 minutes or more. What does happen is a steady stream of failed sign-ins on the account: providers that count them per day, or send a "blocked sign-in attempt" mail for each, react to it, and the person's other clients can be caught in a lockout the stream contributes to.
The scout's other point, "two queries per message" in
matching.py:169-185(match): it runs only for a message the rules recognised (sync.py:377-380), which is a handful per pass; the thread and contact queries are indexed lookups. Not worth an issue; dropped.What would change
In the core:
is_due, whenlast_erroris set, wait the longer of the interval and the time the connection has been failing (synced_at - last_ok_at, or sincecreated_atwhen it never worked), capped at one day. That is an exponential back-off with no new column: 1 h, 2 h, 4 h … 24 h, and the next success puts it back on its interval. Sync now is unaffected, since it callssync_connectiondirectly. The connection card should say when the next attempt is, so the slowdown is not mistaken for a hang (a server-rendered sentence beside the existing last-error text).ConnectionUnusablefor syncs. In_run_connection, catch it as the store path does and callconnection.retire(str(dead), keep_secrets=dead.keep_secrets).In postulo-imap, only if the user chooses to retire (Decisions): raise
ConnectionUnusable(…, keep_secrets=True)when theLOGINanswer carries the RFC 5530[AUTHENTICATIONFAILED]response code, and keep the ordinary error for[UNAVAILABLE]and for servers that send no code. That would be a separate, small issue on postulo-imap.No library: the back-off is three lines of arithmetic on fields the row already has.
What it saves or protects
medium (reliability): a connection whose password was changed stops dialling the mail, DAV or France Travail server 24 times a day (96 at the 15-minute interval) with a credential known to fail
Accessibility
none
Decided by the maintainer (2026-10-31)
back off every failing sync, capped at one day; never retire a connection on its own
Decisions
ConnectionUnusablefor syncs, one PR); the matching queries are not filed.Found by the efficiency and reliability audit of 2026-10-30 (finding PR3; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).