112 lines
3.5 KiB
Go
112 lines
3.5 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, config Config) (*Bus, error) {
|
|
if err := config.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
settings := reliable.DurableSettings{Enabled: true, Stream: config.Stream, Provisioning: config.Provisioning}
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|