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
|`aboutbits.emailservice.scheduling.enabled`| true | Enables the scheduler sending the emails. |
81
+
|`aboutbits.emailservice.scheduling.cleanup.enabled`| true | Enables cleanup of attachment files after sending. |
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. |
85
+
86
+
## Multi-pod deployments
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.
0 commit comments