All checks were successful
check / check (push) Successful in 33s
notify.New accepted an fx.Lifecycle and never used it, so the three dispatch goroutines were untracked. context.WithoutCancel kept a delivery alive past its caller's cancellation but made nothing wait for it: the process could exit while a delivery was still in its retry backoff (up to five attempts, 60s max delay), silently losing exactly the alert most worth keeping. Deliveries are now tracked in a sync.WaitGroup whose counter is incremented on the dispatching goroutine before the worker starts, and notify.New registers an OnStop hook that drains them. The drain is bounded by the context fx passes to OnStop; when it expires with work outstanding, the count is logged at warn level and parked retry backoffs are released via an abandon channel so they stop retrying rather than outliving the drain. Deliveries submitted after the drain has begun are refused and logged, so a stream of new notifications cannot extend shutdown indefinitely. The three near-identical dispatchers now share one tracked dispatch helper. Tests use httptest servers and the existing retry knobs (SetRetryConfig/SetSleepFunc) so nothing waits on a real backoff. README's shutdown claim is reworded to match the bounded semantics.
122 lines
2.9 KiB
Go
122 lines
2.9 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
// NtfyPriority exports ntfyPriority for testing.
|
|
func NtfyPriority(priority string) string {
|
|
return ntfyPriority(priority)
|
|
}
|
|
|
|
// SlackColor exports slackColor for testing.
|
|
func SlackColor(priority string) string {
|
|
return slackColor(priority)
|
|
}
|
|
|
|
// NewRequestForTest exports newRequest for testing.
|
|
func NewRequestForTest(
|
|
ctx context.Context,
|
|
method string,
|
|
target *url.URL,
|
|
body io.Reader,
|
|
) *http.Request {
|
|
return newRequest(ctx, method, target, body)
|
|
}
|
|
|
|
// NewTestService creates a Service suitable for unit testing.
|
|
// It discards log output and uses the given transport.
|
|
func NewTestService(transport http.RoundTripper) *Service {
|
|
return newService(slog.New(slog.DiscardHandler), transport)
|
|
}
|
|
|
|
// NewTestServiceWithLogger creates a Service that writes to the
|
|
// given handler, so tests can assert on emitted log records.
|
|
func NewTestServiceWithLogger(
|
|
transport http.RoundTripper,
|
|
handler slog.Handler,
|
|
) *Service {
|
|
return newService(slog.New(handler), transport)
|
|
}
|
|
|
|
// Drain exports drain for testing.
|
|
func (svc *Service) Drain(ctx context.Context) {
|
|
svc.drain(ctx)
|
|
}
|
|
|
|
// OutstandingDeliveries reports how many delivery goroutines
|
|
// are currently tracked as in flight.
|
|
func (svc *Service) OutstandingDeliveries() int64 {
|
|
return svc.outstanding.Load()
|
|
}
|
|
|
|
// SetNtfyURL sets the ntfy URL on a Service for testing.
|
|
func (svc *Service) SetNtfyURL(u *url.URL) {
|
|
svc.ntfyURL = u
|
|
}
|
|
|
|
// SetSlackWebhookURL sets the Slack webhook URL on a
|
|
// Service for testing.
|
|
func (svc *Service) SetSlackWebhookURL(u *url.URL) {
|
|
svc.slackWebhookURL = u
|
|
}
|
|
|
|
// SetMattermostWebhookURL sets the Mattermost webhook URL on
|
|
// a Service for testing.
|
|
func (svc *Service) SetMattermostWebhookURL(u *url.URL) {
|
|
svc.mattermostWebhookURL = u
|
|
}
|
|
|
|
// SendNtfy exports sendNtfy for testing.
|
|
func (svc *Service) SendNtfy(
|
|
ctx context.Context,
|
|
topicURL *url.URL,
|
|
title, message, priority string,
|
|
) error {
|
|
return svc.sendNtfy(ctx, topicURL, title, message, priority)
|
|
}
|
|
|
|
// SendSlack exports sendSlack for testing.
|
|
func (svc *Service) SendSlack(
|
|
ctx context.Context,
|
|
webhookURL *url.URL,
|
|
title, message, priority string,
|
|
) error {
|
|
return svc.sendSlack(
|
|
ctx, webhookURL, title, message, priority,
|
|
)
|
|
}
|
|
|
|
// SetRetryConfig overrides the retry configuration for
|
|
// testing.
|
|
func (svc *Service) SetRetryConfig(cfg RetryConfig) {
|
|
svc.retryConfig = cfg
|
|
}
|
|
|
|
// SetSleepFunc overrides the sleep function so tests can
|
|
// eliminate real delays.
|
|
func (svc *Service) SetSleepFunc(
|
|
fn func(time.Duration) <-chan time.Time,
|
|
) {
|
|
svc.sleepFn = fn
|
|
}
|
|
|
|
// DeliverWithRetry exports deliverWithRetry for testing.
|
|
func (svc *Service) DeliverWithRetry(
|
|
ctx context.Context,
|
|
endpoint string,
|
|
fn func(context.Context) error,
|
|
) error {
|
|
return svc.deliverWithRetry(ctx, endpoint, fn)
|
|
}
|
|
|
|
// BackoffDuration exports RetryConfig.backoff for testing.
|
|
func (rc RetryConfig) BackoffDuration(attempt int) time.Duration {
|
|
return rc.defaults().backoff(attempt)
|
|
}
|