internal/notify/notify.go

70dc0648f931f6f7112c6b71b0be485eeae4077f
gitbay/internal/notify/notify.go history · blame · raw

67 lines · 1903 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
16// DefaultMaxAttempts and DefaultRetryBase are the retry parameters gitbayd
17// wires up in production (cmd/gitbayd/main.go), named so anything that
18// needs to reason about the mailer's worst-case delivery time — such as
19// checking it against a login link's TTL — computes it from the numbers
20// actually in force rather than a copy of them.
21const (
22	DefaultMaxAttempts = 5
23	DefaultRetryBase   = 30 * time.Second
24)
25
26type Mailer struct {
27	St          *store.Store
28	Cfg         config.Config
29	RetryBase   time.Duration
30	MaxAttempts int
31}
32
33func New(st *store.Store, cfg config.Config, retryBase time.Duration) *Mailer {
34	return &Mailer{St: st, Cfg: cfg, RetryBase: retryBase, MaxAttempts: DefaultMaxAttempts}
35}
36
37// Run polls for due mail until ctx is done.
38func (m *Mailer) Run(ctx context.Context) {
39	tick := time.NewTicker(2 * time.Second)
40	defer tick.Stop()
41	for {
42		select {
43		case <-ctx.Done():
44			return
45		case <-tick.C:
46			due, err := m.St.DueMail(20)
47			if err != nil {
48				slog.Error("notify: listing due mail", "err", err)
49				continue
50			}
51			for _, q := range due {
52				if err := mail.Send(m.Cfg, q.Recipient, q.Subject, q.Body); err != nil {
53					attempt := q.Attempts + 1
54					if attempt >= m.MaxAttempts {
55						m.St.MarkMailFailed(q.ID, err.Error(), nil)
56						slog.Warn("notification dead-lettered", "recipient", q.Recipient, "err", err)
57					} else {
58						next := time.Now().Add(m.RetryBase << (attempt - 1))
59						m.St.MarkMailFailed(q.ID, err.Error(), &next)
60					}
61					continue
62				}
63				m.St.MarkMailSent(q.ID)
64			}
65		}
66	}
67}