Skip to content

Commit e429e20

Browse files
committed
update readme.md
1 parent 7d6526f commit e429e20

1 file changed

Lines changed: 34 additions & 5 deletions

File tree

readme.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,40 @@ public class App {
7474

7575
The following configuration options are available:
7676

77-
| Name | Default | Description |
78-
|----------------------------------------|-------------|-----------------------------------------------------------------------|
79-
| `lib.emailservice.migrations.enabled` | true | Enables database migrations. |
80-
| `lib.emailservice.scheduling.enabled` | true | Enables the scheduler sending the emails. |
81-
| `lib.emailservice.scheduling.interval` | 30000 | Specifies the milliseconds delay between runs of the scheduler. |
77+
| Name | Default | Description |
78+
|-------------------------------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------|
79+
| `aboutbits.emailservice.migrations.enabled` | true | Enables database migrations. |
80+
| `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.
82111

83112
## Local development:
84113

0 commit comments

Comments
 (0)