You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: readme.md
+4-25Lines changed: 4 additions & 25 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -80,34 +80,13 @@ The following configuration options are available:
80
80
|`aboutbits.emailservice.scheduling.enabled`| true | Enables the scheduler sending the emails. |
81
81
|`aboutbits.emailservice.scheduling.cleanup.enabled`| true | Enables cleanup of attachment files after sending. |
82
82
|`aboutbits.emailservice.scheduling.interval`| 30000 | Milliseconds delay between runs of the scheduler. |
83
-
|`aboutbits.emailservice.scheduling.stuck-sending-recovery-threshold`|PT5M| How long an email may stay in `SENDING` before being considered abandoned (crashed pod) and eligible to be re-claimed. |
84
-
|`aboutbits.emailservice.scheduling.max-attempts`| 3 | Maximum number of send attempts before an email is marked as `ERROR`. Applies only to the scheduled retry loop. |
83
+
|`aboutbits.emailservice.scheduling.stuck-sending-recovery-threshold`|PT30M| How long an email may stay in `SENDING` before being considered abandoned (crashed pod) and eligible to be re-claimed. Must comfortably exceed the worst-case SMTP send duration: JavaMail's default connect/read/write timeouts are infinite, so configure `spring.mail.properties.mail.smtp.connectiontimeout`, `spring.mail.properties.mail.smtp.timeout` and `spring.mail.properties.mail.smtp.writetimeout` well below this threshold, otherwise a slow in-flight send can be re-claimed by another pod and delivered twice. |
84
+
|`aboutbits.emailservice.scheduling.max-attempts`| 3 | Maximum number of send attempts before an email is marked as `ERROR`. Applies only to the scheduled retry loop. Failed attempts are retried with exponential backoff (`scheduling.interval` × 2^attempts); with the defaults a persistently failing email runs attempt 1 → +60s → attempt 2 → +120s → attempt 3 → `ERROR`.|
85
85
86
86
## Multi-pod deployments
87
87
88
-
The scheduler is safe to run on every pod concurrently. Each pass performs
89
-
three independent steps:
90
-
91
-
1.**Candidate scan** — a plain `SELECT` returns up to `batch-size` ids of
92
-
emails whose `scheduled_at` has passed and whose state is `PENDING` or `SENDING`
93
-
older than `stuck-sending-recovery-threshold` (crashed-pod recovery). `ERROR`
94
-
is terminal: rows that exhausted `max-attempts` are never re-picked
95
-
automatically — an operator can reset them to `PENDING` if a retry is desired.
96
-
Two pods may see overlapping ids at this step, and that's fine — the atomic
97
-
claim below arbitrates.
98
-
2.**Atomic claim** — for each candidate id, a single compare-and-set
99
-
`UPDATE … SET state = SENDING … WHERE id = ? AND state IN (PENDING, ERROR, staleSENDING)`
100
-
is issued. The database serializes concurrent updates against the same row so
101
-
exactly one pod sees `rowsAffected = 1` and owns that email; the other pods
102
-
see `0` and move on.
103
-
3.**Send and persist** — the winning pod calls SMTP outside any database
104
-
transaction and then writes the final `SENT` / `ERROR` / `PENDING` state through a small `save()`.
105
-
106
-
Crash recovery: if a pod dies between "claim" and "persist result", the row
107
-
stays in `SENDING` with its `execution_start_time` frozen. After
108
-
`stuck-sending-recovery-threshold` any pod's next pass picks it up as a
109
-
candidate again and retries. Repeated crashes therefore count against
110
-
`max-attempts` and eventually escalate the row to `ERROR` rather than looping forever.
88
+
The scheduler is safe to run on every pod concurrently: the database arbitrates which pod sends each email.
89
+
Delivery is at-least-once - if a pod crashes after the SMTP server accepted the message but before the result was persisted, the email may be sent again on recovery.
"description": "How long an email may stay in the SENDING state before being considered abandoned (crashed pod) and eligible to be re-claimed by another pod.",
31
-
"defaultValue": "PT5M"
30
+
"description": "How long an email may stay in the SENDING state before being considered abandoned (crashed pod) and eligible to be re-claimed by another pod. Must comfortably exceed the worst-case SMTP send duration: JavaMail's default connect/read/write timeouts are infinite, so configure spring.mail.properties.mail.smtp.connectiontimeout, spring.mail.properties.mail.smtp.timeout and spring.mail.properties.mail.smtp.writetimeout well below this threshold, otherwise a slow in-flight send can be re-claimed by another pod and delivered twice.",
"description": "Maximum number of send attempts before an email is marked as ERROR. Applies to the scheduled retry loop; the synchronous sendOrFail path is fail-fast and does not retry.",
36
+
"description": "Maximum number of send attempts before an email is marked as ERROR. Applies to the scheduled retry loop; the synchronous sendOrFail path is fail-fast and does not retry. Failed attempts are retried with exponential backoff (scheduling.interval x 2^attempts); with the defaults a persistently failing email runs attempt 1 -> +60s -> attempt 2 -> +120s -> attempt 3 -> ERROR.",
0 commit comments