Files
sxchnge/backend/notifications/bus/dispatch.go
T

109 lines
3.7 KiB
Go

// Package bus connects the notification outbox to the shared messaging runtime.
package bus
import (
"context"
"strconv"
"time"
"git.sendico.io/tech/sxchnge/notifications/model"
merrors "git.sendico.io/tech/sxchnge/shared/errors"
mlogger "git.sendico.io/tech/sxchnge/shared/logging"
"git.sendico.io/tech/sxchnge/shared/messaging/reliable"
)
const subject = "sxchnge.notifications.email"
type Outbox interface {
Due(context.Context, time.Time) ([]model.Notification, error)
Recover(context.Context, time.Time) error
}
type Handler interface {
Dispatch(context.Context, string) (bool, error)
}
type Bus struct {
transport reliable.Transport
subscription reliable.Subscription
outbox Outbox
handler Handler
logger mlogger.Logger
}
func New(ctx context.Context, transport reliable.Transport, admin reliable.Admin, outbox Outbox, handler Handler, logger mlogger.Logger) (*Bus, error) {
settings := reliable.DurableSettings{Enabled: true, Stream: reliable.DurableStreamSettings{Name: "SXCHNGE_NOTIFICATIONS", Subjects: []string{subject}, Storage: "file", MaxAgeHours: 48, Replicas: 1}, Provisioning: reliable.DurableProvisioningSettings{Mode: reliable.DurableProvisioningApp, Environment: reliable.DurableEnvironmentNonProduction}}
consumer := reliable.DurableConsumerSettings{Durable: "email", Subject: subject, AckWaitSeconds: 60, MaxDeliver: -1, DeliverPolicy: reliable.DurableDeliverAll}
if err := admin.EnsureConsumerResources(ctx, settings, consumer); err != nil {
return nil, merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "configure notification messaging")
}
subscription, err := transport.OpenConsumer(ctx, settings, consumer)
if err != nil {
return nil, merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "subscribe notification worker")
}
return &Bus{transport, subscription, outbox, handler, mlogger.Named(logger, "notifications", "bus")}, nil
}
func (b *Bus) Close() error { return b.subscription.Close() }
func (b *Bus) PublishDue(ctx context.Context) error {
if err := b.outbox.Recover(ctx, time.Now().UTC()); err != nil {
return err
}
jobs, err := b.outbox.Due(ctx, time.Now().UTC())
if err != nil {
return err
}
for _, job := range jobs {
err = b.transport.Publish(ctx, reliable.Record{Subject: subject, Payload: []byte(job.ID.Hex()), MessageID: job.ID.Hex() + "-" + strconv.FormatUint(uint64(job.Attempts), 10)})
if err != nil {
return merrors.OpaqueWrap(merrors.ErrServiceUnavailable, err, "publish notification job")
}
}
return nil
}
func (b *Bus) RunPublisher(ctx context.Context) {
for ctx.Err() == nil {
op, cancel := context.WithTimeout(ctx, 10*time.Second)
err := b.PublishDue(op)
cancel()
if err != nil && ctx.Err() == nil {
b.logger.Warn("Notification outbox publication failed")
}
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
}
}
}
func (b *Bus) RunWorker(ctx context.Context) {
for ctx.Err() == nil {
deliveries, err := b.subscription.Fetch(ctx, 1, time.Second)
if err != nil {
if ctx.Err() == nil {
b.logger.Warn("Notification job fetch failed")
}
select {
case <-ctx.Done():
return
case <-time.After(time.Second):
}
continue
}
for _, delivery := range deliveries {
payload := delivery.Record().Payload
if len(payload) == 24 {
if _, err = b.handler.Dispatch(ctx, string(payload)); err != nil {
b.logger.Warn("Notification job processing failed")
continue
}
} else {
b.logger.Warn("Malformed notification job discarded")
}
ackCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
err = delivery.Ack(ackCtx)
cancel()
if err != nil {
b.logger.Warn("Notification acknowledgement failed")
}
}
}
}