Implement notifications service with shared messaging and email drivers (#36)
This commit is contained in:
1 parent
36277b6c45
commit
5c2d36281b
54 files changed
+3746
-6
No files matched your search
@@ -0,0 +1,178 @@
|
||||
package natscap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"git.sendico.io/tech/sxchnge/shared/messaging/capability"
|
||||
"git.sendico.io/tech/sxchnge/shared/messaging/reliable"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type natsSubscription struct {
|
||||
mu sync.Mutex
|
||||
sub *nats.Subscription
|
||||
closed bool
|
||||
}
|
||||
|
||||
func fetchPollContext(ctx context.Context, wait time.Duration) (context.Context, context.CancelFunc, bool) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if wait <= 0 {
|
||||
return ctx, nil, false
|
||||
}
|
||||
if deadline, hasDeadline := ctx.Deadline(); hasDeadline && time.Until(deadline) <= wait {
|
||||
return ctx, nil, false
|
||||
}
|
||||
nextCtx, cancel := context.WithTimeout(ctx, wait)
|
||||
return nextCtx, cancel, true
|
||||
}
|
||||
|
||||
func (s *natsSubscription) Fetch(ctx context.Context, limit int, wait time.Duration) ([]reliable.Delivery, error) {
|
||||
if limit <= 0 {
|
||||
return nil, merrors.InvalidArgument("fetch limit must be greater than zero", "limit")
|
||||
}
|
||||
s.mu.Lock()
|
||||
closed := s.closed
|
||||
sub := s.sub
|
||||
s.mu.Unlock()
|
||||
if closed || sub == nil {
|
||||
return nil, capability.ErrClosed
|
||||
}
|
||||
fetchCtx, cancel, derivedDeadline := fetchPollContext(ctx, wait)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
msgs, err := sub.Fetch(limit, nats.Context(fetchCtx))
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, nats.ErrTimeout):
|
||||
return nil, nil
|
||||
case derivedDeadline && errors.Is(err, context.DeadlineExceeded):
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
closed = s.closed
|
||||
s.mu.Unlock()
|
||||
if closed {
|
||||
return nil, capability.ErrClosed
|
||||
}
|
||||
result := make([]reliable.Delivery, 0, len(msgs))
|
||||
for _, msg := range msgs {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, &natsDelivery{sub: s, msg: msg, record: messageRecord(msg), attempt: deliveryAttempt(msg)})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *natsSubscription) Close() error {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
s.closed = true
|
||||
sub := s.sub
|
||||
s.sub = nil
|
||||
s.mu.Unlock()
|
||||
if sub == nil {
|
||||
return nil
|
||||
}
|
||||
if err := sub.Unsubscribe(); err != nil && !errors.Is(err, nats.ErrBadSubscription) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *natsSubscription) isClosed() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.closed
|
||||
}
|
||||
|
||||
type natsDelivery struct {
|
||||
sub *natsSubscription
|
||||
msg *nats.Msg
|
||||
record reliable.Record
|
||||
attempt int
|
||||
|
||||
mu sync.Mutex
|
||||
acked bool
|
||||
}
|
||||
|
||||
func (d *natsDelivery) Record() reliable.Record {
|
||||
return d.record
|
||||
}
|
||||
|
||||
func (d *natsDelivery) Attempt() int {
|
||||
if d.attempt <= 0 {
|
||||
return 1
|
||||
}
|
||||
return d.attempt
|
||||
}
|
||||
|
||||
func (d *natsDelivery) Ack(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
if d.acked {
|
||||
d.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if d.sub != nil && d.sub.isClosed() {
|
||||
d.mu.Unlock()
|
||||
return capability.ErrClosed
|
||||
}
|
||||
msg := d.msg
|
||||
d.mu.Unlock()
|
||||
if msg == nil {
|
||||
return capability.ErrClosed
|
||||
}
|
||||
if err := msg.AckSync(nats.Context(ctx)); err != nil {
|
||||
return err
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.acked = true
|
||||
d.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageRecord(msg *nats.Msg) reliable.Record {
|
||||
headers := make(map[string][]string, len(msg.Header)+1)
|
||||
for key, values := range msg.Header {
|
||||
headers[strings.ToLower(strings.TrimSpace(key))] = append([]string(nil), values...)
|
||||
}
|
||||
messageID := strings.TrimSpace(msg.Header.Get(nats.MsgIdHdr))
|
||||
if businessID := reliable.HeaderValue(headers, reliable.HeaderOriginalBusinessMsgID); businessID != "" {
|
||||
messageID = businessID
|
||||
}
|
||||
headers[reliable.HeaderTransportMessageID] = []string{strings.TrimSpace(msg.Header.Get(nats.MsgIdHdr))}
|
||||
return reliable.Record{
|
||||
Subject: strings.TrimSpace(msg.Subject),
|
||||
Payload: append([]byte(nil), msg.Data...),
|
||||
Headers: headers,
|
||||
MessageID: strings.TrimSpace(messageID),
|
||||
}
|
||||
}
|
||||
|
||||
func deliveryAttempt(msg *nats.Msg) int {
|
||||
if msg == nil {
|
||||
return 1
|
||||
}
|
||||
meta, err := msg.Metadata()
|
||||
if err != nil || meta == nil || meta.NumDelivered == 0 {
|
||||
return 1
|
||||
}
|
||||
if meta.NumDelivered > uint64(^uint(0)>>1) {
|
||||
return int(^uint(0) >> 1)
|
||||
}
|
||||
return int(meta.NumDelivered)
|
||||
}
|
||||
Reference in new issue
Block a user