internal/notify/notify.go

96df83f2d3eb9f241bcaa53fcc243d090c53ab2b
gitbay/internal/notify/notify.go history · blame · raw

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}