118 lines
4.4 KiB
Go
118 lines
4.4 KiB
Go
package db
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// RelayQueueRecipient is one entry of a RelayQueueItem's recipients_json — mirrors
|
|
// relay.Result's Recipient/RecipientType shape closely enough to round-trip without
|
|
// internal/db needing to import internal/relay (which would be a cycle: relay already
|
|
// imports db).
|
|
type RelayQueueRecipient struct {
|
|
Recipient string `json:"recipient"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// RelayQueueItem is one recipient-domain-group of in-flight outbound relay work — see
|
|
// schema.go's esrv_relay_queue comment.
|
|
type RelayQueueItem struct {
|
|
ID int64
|
|
EmailLogID int64
|
|
MailFrom string
|
|
Domain string
|
|
Recipients []RelayQueueRecipient
|
|
Content string
|
|
Status string
|
|
Attempts int
|
|
NextAttemptAt time.Time
|
|
LastError string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// InsertRelayQueueItem queues one recipient-domain-group for delivery, due at
|
|
// nextAttemptAt (normally time.Now() for a fresh message — the worker's next tick
|
|
// picks it up promptly).
|
|
func (d *DB) InsertRelayQueueItem(emailLogID int64, mailFrom, domain string, recipients []RelayQueueRecipient, content string, nextAttemptAt time.Time) (int64, error) {
|
|
recipientsJSON, err := json.Marshal(recipients)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
res, err := d.Exec(`INSERT INTO esrv_relay_queue
|
|
(email_log_id, mail_from, domain, recipients_json, content, next_attempt_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
emailLogID, mailFrom, domain, string(recipientsJSON), content, nextAttemptAt)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
// ClaimDueRelayQueueItems selects up to limit rows that are due (status='pending' and
|
|
// next_attempt_at in the past) and immediately flips them to status='sending' so a
|
|
// second worker tick can't pick the same row up again — a plain select-then-update,
|
|
// not a single atomic statement, but safe here: sqlDB.SetMaxOpenConns(1) (schema.go)
|
|
// already serializes every DB access app-wide through one connection, so there's no
|
|
// real cross-goroutine race to guard against, only "don't process the same row twice
|
|
// within one process," which this already prevents.
|
|
func (d *DB) ClaimDueRelayQueueItems(limit int) ([]RelayQueueItem, error) {
|
|
rows, err := d.Query(`SELECT id, email_log_id, mail_from, domain, recipients_json, content, attempts, next_attempt_at, created_at
|
|
FROM esrv_relay_queue WHERE status = 'pending' AND next_attempt_at <= ? ORDER BY next_attempt_at LIMIT ?`,
|
|
time.Now(), limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var items []RelayQueueItem
|
|
for rows.Next() {
|
|
var it RelayQueueItem
|
|
var recipientsJSON, nextAttemptAt, createdAt string
|
|
if err := rows.Scan(&it.ID, &it.EmailLogID, &it.MailFrom, &it.Domain, &recipientsJSON, &it.Content, &it.Attempts, &nextAttemptAt, &createdAt); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal([]byte(recipientsJSON), &it.Recipients); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
it.NextAttemptAt, _ = parseTime(nextAttemptAt)
|
|
it.CreatedAt, _ = parseTime(createdAt)
|
|
it.Status = "sending"
|
|
items = append(items, it)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
rows.Close()
|
|
|
|
for _, it := range items {
|
|
if _, err := d.Exec(`UPDATE esrv_relay_queue SET status = 'sending' WHERE id = ?`, it.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// RescheduleRelayQueueItem records a failed delivery attempt and puts the row back to
|
|
// 'pending' at the given next attempt time (retry backoff — see
|
|
// internal/relay/queue.go's retrySchedule).
|
|
func (d *DB) RescheduleRelayQueueItem(id int64, nextAttemptAt time.Time, lastError string) error {
|
|
_, err := d.Exec(`UPDATE esrv_relay_queue SET status = 'pending', attempts = attempts + 1, next_attempt_at = ?, last_error = ? WHERE id = ?`,
|
|
nextAttemptAt, lastError, id)
|
|
return err
|
|
}
|
|
|
|
func (d *DB) DeleteRelayQueueItem(id int64) error {
|
|
_, err := d.Exec(`DELETE FROM esrv_relay_queue WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
// CountPendingRelayQueueItemsForEmailLog reports how many domain-groups for one
|
|
// message are still unresolved — the worker uses this to know when it's the one
|
|
// finalizing an email_log's overall status (all groups either delivered or given up).
|
|
func (d *DB) CountPendingRelayQueueItemsForEmailLog(emailLogID int64) (int, error) {
|
|
var n int
|
|
err := d.QueryRow(`SELECT COUNT(*) FROM esrv_relay_queue WHERE email_log_id = ?`, emailLogID).Scan(&n)
|
|
return n, err
|
|
}
|