Skip to content

Trash: a delivery fmsgd has given up on no longer keeps a message - #59

Merged
markmnl merged 1 commit into
mainfrom
pending-age
Oct 29, 2026
Merged

markmnl merged 1 commit into
mainfrom
pending-age

Conversation

@markmnl

@markmnl markmnl commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Problem

fmsgd stops retrying a delivery once it is older than FMSG_RETRY_MAX_AGE (one day by default), but it never marks the row: a host that never answered leaves time_delivered null and response_code at -1 forever. Delete for good (removeIfUnheld) counted every such row as pending with no age limit, and any add-to participant notification with time_notified null whatever its code. A message sent to a dead or mistyped domain therefore could never be removed from the host: its body and attachments stayed on disk after every local participant had deleted it for good.

The mirror was wrong too. fmsgd also retries reject codes 3 (undisclosed), 5 (insufficient resources) and 101 (user full); the Web API treated those as finished, so it could remove a message, and its delivery rows, while fmsgd was still delivering it.

Change

  • dd.sql: fmsg_api_delivery_pending(msg, now, max_age, retryable) is the rule, next to fmsg_api_trash_state: a msg_to, msg_add_to or msg_add_to_notify row is pending while undelivered, its last code is null or retryable, and it is younger than the window measured from msg.time_sent or msg_add_to_batch.time_added, exactly as fmsgd's sender measures it.
  • trash.go: retryableResponseCodes (-1, 3, 5, 101) with a note that it must match fmsgd's list; removeIfUnheld calls the function.
  • MessageHandler.RetryMaxAge, read from FMSG_RETRY_MAX_AGE in main (default 86400, must be positive). README documents it as a setting to keep equal to fmsgd's, and the Delete for good section says what pending means.

Tests

  • TestPurgeKeepsRetryableDelivery: a delivery that last got 101 keeps the message; one that got 100 does not.
  • TestPurgeRemovesGivenUpDelivery: with a short window, a -1 delivery inside it keeps the message; past it, an add-to delivery and participant notification that fmsgd gave up on no longer do, and the message leaves the host.

Both fail on main. go test ./... passes against Postgres 17 with fmsgd's dd.sql.

Not in this PR

A message deleted for good while a delivery was still inside the window is never re-checked once the window closes; nothing sweeps those. Worth a periodic sweep as a follow-up.

🤖 Generated with Claude Code

fmsgd stops retrying a delivery once it is older than FMSG_RETRY_MAX_AGE
(one day by default) but never marks it: a host that never answered
leaves time_delivered null and response_code -1 for good. Delete for
good treated every such row as pending with no age limit, and any
add-to participant notification with time_notified null whatever its
code, so a message sent to a dead or mistyped domain could never be
removed from the host. The mirror was wrong too: fmsgd also retries
codes 3, 5 and 101, which were treated as finished, so a message could
be removed while fmsgd was still delivering it.

Pending now means what it means to fmsgd: undelivered, last code null or
one fmsgd retries (-1, 3, 5, 101), and younger than the retry window
measured from the message's time_sent or the add-to batch's time_added.
The rule is fmsg_api_delivery_pending in dd.sql, used by removeIfUnheld;
the retryable codes are one list in trash.go that must match fmsgd's.
FMSG_RETRY_MAX_AGE is read by the Web API as well (same default) and
documented as a setting to keep equal on both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@markmnl
markmnl merged commit 2a7abd1 into main Oct 29, 2026
1 check passed
@markmnl
markmnl deleted the pending-age branch October 29, 2026 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant