internal/notify/notify.go
57 lines · 1465 bytes
1// Package notify drains the notification queue: activity mail with the same
2// bounded-retry discipline as webhook delivery, so a flaky relay delays
3// feedback instead of losing it.
4package notify
5
6import (
7 "context"
8 "log/slog"
9 "time"
10
11 "gitbay.org/gitbay/internal/config"
12 "gitbay.org/gitbay/internal/mail"
13 "gitbay.org/gitbay/internal/store"
14)
15
16type Mailer struct {
17 St *store.Store
18 Cfg config.Config
19 RetryBase time.Duration
20 MaxAttempts int
21}
22
23func New(st *store.Store, cfg config.Config, retryBase time.Duration) *Mailer {
24 return &Mailer{St: st, Cfg: cfg, RetryBase: retryBase, MaxAttempts: 5}
25}
26
27// Run polls for due mail until ctx is done.
28func (m *Mailer) Run(ctx context.Context) {
29 tick := time.NewTicker(2 * time.Second)
30 defer tick.Stop()
31 for {
32 select {
33 case <-ctx.Done():
34 return
35 case <-tick.C:
36 due, err := m.St.DueMail(20)
37 if err != nil {
38 slog.Error("notify: listing due mail", "err", err)
39 continue
40 }
41 for _, q := range due {
42 if err := mail.Send(m.Cfg, q.Recipient, q.Subject, q.Body); err != nil {
43 attempt := q.Attempts + 1
44 if attempt >= m.MaxAttempts {
45 m.St.MarkMailFailed(q.ID, err.Error(), nil)
46 slog.Warn("notification dead-lettered", "recipient", q.Recipient, "err", err)
47 } else {
48 next := time.Now().Add(m.RetryBase << (attempt - 1))
49 m.St.MarkMailFailed(q.ID, err.Error(), &next)
50 }
51 continue
52 }
53 m.St.MarkMailSent(q.ID)
54 }
55 }
56 }
57}