// 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") } } } }