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,114 @@
|
||||
package natscap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/messaging/capability"
|
||||
basenats "git.sendico.io/tech/sxchnge/shared/messaging/internal/natsb"
|
||||
"git.sendico.io/tech/sxchnge/shared/messaging/reliable"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type DurableAdapter struct {
|
||||
broker *basenats.NatsBroker
|
||||
}
|
||||
|
||||
type durableAdminContext interface {
|
||||
AddStream(cfg *nats.StreamConfig, opts ...nats.JSOpt) (*nats.StreamInfo, error)
|
||||
UpdateStream(cfg *nats.StreamConfig, opts ...nats.JSOpt) (*nats.StreamInfo, error)
|
||||
StreamInfo(stream string, opts ...nats.JSOpt) (*nats.StreamInfo, error)
|
||||
AddConsumer(stream string, cfg *nats.ConsumerConfig, opts ...nats.JSOpt) (*nats.ConsumerInfo, error)
|
||||
UpdateConsumer(stream string, cfg *nats.ConsumerConfig, opts ...nats.JSOpt) (*nats.ConsumerInfo, error)
|
||||
ConsumerInfo(stream, consumer string, opts ...nats.JSOpt) (*nats.ConsumerInfo, error)
|
||||
}
|
||||
|
||||
func NewDurableAdapter(broker *basenats.NatsBroker) *DurableAdapter {
|
||||
if broker == nil || broker.JetStream() == nil {
|
||||
return nil
|
||||
}
|
||||
return &DurableAdapter{broker: broker}
|
||||
}
|
||||
|
||||
func (a *DurableAdapter) Publish(ctx context.Context, record reliable.Record) error {
|
||||
if err := reliable.ValidatePublicRecord(record); err != nil {
|
||||
return err
|
||||
}
|
||||
normalized, err := reliable.NormalizePublicRecord(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.publishRecord(ctx, normalized)
|
||||
}
|
||||
|
||||
func (a *DurableAdapter) PublishRuntime(ctx context.Context, record reliable.Record) error {
|
||||
normalized, err := reliable.NormalizeRuntimeRecord(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.publishRecord(ctx, normalized)
|
||||
}
|
||||
|
||||
func (a *DurableAdapter) publishRecord(ctx context.Context, record reliable.Record) error {
|
||||
js := a.jetStream()
|
||||
if js == nil {
|
||||
return capability.Unavailable("durable transport")
|
||||
}
|
||||
msg := &nats.Msg{
|
||||
Subject: strings.TrimSpace(record.Subject),
|
||||
Data: append([]byte(nil), record.Payload...),
|
||||
Header: nats.Header{},
|
||||
}
|
||||
for key, values := range record.Headers {
|
||||
for _, value := range values {
|
||||
msg.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
if record.MessageID != "" {
|
||||
msg.Header.Set(nats.MsgIdHdr, record.MessageID)
|
||||
}
|
||||
_, err := js.PublishMsg(msg, nats.Context(ctx))
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *DurableAdapter) OpenConsumer(ctx context.Context, settings reliable.DurableSettings, consumer reliable.DurableConsumerSettings) (reliable.Subscription, error) {
|
||||
js := a.jetStream()
|
||||
if js == nil {
|
||||
return nil, capability.Unavailable("durable transport")
|
||||
}
|
||||
sub, err := js.PullSubscribe(
|
||||
consumer.Subject,
|
||||
consumer.Durable,
|
||||
nats.Bind(settings.Stream.Name, consumer.Durable),
|
||||
nats.Context(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &natsSubscription{sub: sub}, nil
|
||||
}
|
||||
|
||||
func (a *DurableAdapter) EnsureProducerResources(_ context.Context, settings reliable.DurableSettings) error {
|
||||
if a == nil || a.broker == nil {
|
||||
return capability.Unavailable("durable admin")
|
||||
}
|
||||
return ensureDurableStream(a.broker.Logger(), a.jetStream(), settings)
|
||||
}
|
||||
|
||||
func (a *DurableAdapter) EnsureConsumerResources(_ context.Context, settings reliable.DurableSettings, consumer reliable.DurableConsumerSettings) error {
|
||||
if a == nil || a.broker == nil {
|
||||
return capability.Unavailable("durable admin")
|
||||
}
|
||||
js := a.jetStream()
|
||||
if err := ensureDurableStream(a.broker.Logger(), js, settings); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureDurableConsumerConfig(a.broker.Logger(), js, settings, consumer)
|
||||
}
|
||||
|
||||
func (a *DurableAdapter) jetStream() nats.JetStreamContext {
|
||||
if a == nil || a.broker == nil {
|
||||
return nil
|
||||
}
|
||||
return a.broker.JetStream()
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package natscap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func ensureDurableStream(logger *zap.Logger, js durableAdminContext, settings reliable.DurableSettings) error {
|
||||
if js == nil {
|
||||
return capability.Unavailable("durable admin")
|
||||
}
|
||||
expected := &nats.StreamConfig{
|
||||
Name: settings.Stream.Name,
|
||||
Subjects: append([]string(nil), settings.Stream.Subjects...),
|
||||
Retention: nats.LimitsPolicy,
|
||||
MaxAge: settings.StreamMaxAge(),
|
||||
Replicas: settings.Stream.Replicas,
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(settings.Stream.Storage), "memory") {
|
||||
expected.Storage = nats.MemoryStorage
|
||||
} else {
|
||||
expected.Storage = nats.FileStorage
|
||||
}
|
||||
|
||||
info, err := js.StreamInfo(expected.Name)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrStreamNotFound) {
|
||||
if !settings.AllowsAutoProvision() {
|
||||
if logger != nil {
|
||||
logger.Warn("Durable stream is missing and auto provisioning is disabled", zap.String("stream", expected.Name))
|
||||
}
|
||||
return err
|
||||
}
|
||||
_, err = js.AddStream(expected)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
current := info.Config
|
||||
if err := validateStreamConfig(current, *expected, settings.IsProductionValidation()); err != nil {
|
||||
if settings.AllowsAutoProvision() {
|
||||
reconciled, reconcileErr := reconcileDurableStream(logger, js, current, *expected, settings)
|
||||
if reconciled {
|
||||
return reconcileErr
|
||||
}
|
||||
if reconcileErr != nil {
|
||||
return reconcileErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
if settings.AllowsAutoProvision() && current.MaxAge != expected.MaxAge {
|
||||
current.MaxAge = expected.MaxAge
|
||||
_, err = js.UpdateStream(¤t)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reconcileDurableStream(logger *zap.Logger, js durableAdminContext, current nats.StreamConfig, expected nats.StreamConfig, settings reliable.DurableSettings) (bool, error) {
|
||||
if js == nil {
|
||||
return false, capability.Unavailable("durable admin")
|
||||
}
|
||||
if strings.TrimSpace(current.Name) != strings.TrimSpace(expected.Name) {
|
||||
return false, merrors.DataConflictf("durable stream name drift: got=%s want=%s", current.Name, expected.Name)
|
||||
}
|
||||
if current.Storage != expected.Storage {
|
||||
return false, merrors.DataConflictf("durable stream storage drift: got=%s want=%s", current.Storage.String(), expected.Storage.String())
|
||||
}
|
||||
if current.Retention != expected.Retention {
|
||||
return false, merrors.DataConflictf("durable stream retention drift: got=%s want=%s", current.Retention.String(), expected.Retention.String())
|
||||
}
|
||||
if current.Replicas != expected.Replicas {
|
||||
return false, merrors.DataConflictf("durable stream replicas drift: got=%d want=%d", current.Replicas, expected.Replicas)
|
||||
}
|
||||
missing, extra := stringSetDiff(expected.Subjects, current.Subjects), stringSetDiff(current.Subjects, expected.Subjects)
|
||||
if len(extra) > 0 {
|
||||
return false, merrors.DataConflict("durable stream has unexpected subjects")
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
current.Subjects = append([]string(nil), expected.Subjects...)
|
||||
current.MaxAge = expected.MaxAge
|
||||
_, err := js.UpdateStream(¤t)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func ensureDurableConsumerConfig(logger *zap.Logger, js durableAdminContext, settings reliable.DurableSettings, consumer reliable.DurableConsumerSettings) error {
|
||||
if js == nil {
|
||||
return capability.Unavailable("durable admin")
|
||||
}
|
||||
deliverPolicy, err := natsDeliverPolicy(consumer.DeliverPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expected := &nats.ConsumerConfig{
|
||||
Durable: consumer.Durable,
|
||||
DeliverPolicy: deliverPolicy,
|
||||
AckPolicy: nats.AckExplicitPolicy,
|
||||
AckWait: time.Duration(consumer.AckWaitSeconds) * time.Second,
|
||||
MaxDeliver: consumer.MaxDeliver,
|
||||
FilterSubject: consumer.Subject,
|
||||
ReplayPolicy: nats.ReplayInstantPolicy,
|
||||
}
|
||||
if consumer.InactiveThresholdSeconds > 0 {
|
||||
expected.InactiveThreshold = time.Duration(consumer.InactiveThresholdSeconds) * time.Second
|
||||
}
|
||||
info, err := js.ConsumerInfo(settings.Stream.Name, consumer.Durable)
|
||||
if err != nil {
|
||||
if errors.Is(err, nats.ErrConsumerNotFound) {
|
||||
if !settings.AllowsAutoProvision() {
|
||||
if logger != nil {
|
||||
logger.Warn("Durable consumer is missing and auto provisioning is disabled", zap.String("stream", settings.Stream.Name), zap.String("subject", consumer.Subject), zap.String("durable", consumer.Durable))
|
||||
}
|
||||
return err
|
||||
}
|
||||
_, err = js.AddConsumer(settings.Stream.Name, expected)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
current := info.Config
|
||||
if current.DeliverPolicy != expected.DeliverPolicy && logger != nil {
|
||||
logger.Warn("Durable consumer deliver policy drift requires delete and recreate",
|
||||
zap.String("stream", settings.Stream.Name),
|
||||
zap.String("subject", consumer.Subject),
|
||||
zap.String("durable", consumer.Durable),
|
||||
zap.String("current_deliver_policy", natsDeliverPolicyName(current.DeliverPolicy)),
|
||||
zap.String("expected_deliver_policy", natsDeliverPolicyName(expected.DeliverPolicy)),
|
||||
zap.Bool("recreate_required", true),
|
||||
)
|
||||
}
|
||||
if err := validateConsumerConfig(current, *expected, settings.IsProductionValidation()); err != nil {
|
||||
return err
|
||||
}
|
||||
if settings.AllowsAutoProvision() && (current.AckWait != expected.AckWait || current.MaxDeliver != expected.MaxDeliver || current.InactiveThreshold != expected.InactiveThreshold) {
|
||||
current.AckWait = expected.AckWait
|
||||
current.MaxDeliver = expected.MaxDeliver
|
||||
current.InactiveThreshold = expected.InactiveThreshold
|
||||
_, err = js.UpdateConsumer(settings.Stream.Name, ¤t)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func natsDeliverPolicy(deliveryMode reliable.DurableDeliverPolicy) (nats.DeliverPolicy, error) {
|
||||
switch deliveryMode {
|
||||
case "", reliable.DurableDeliverAll:
|
||||
return nats.DeliverAllPolicy, nil
|
||||
case reliable.DurableDeliverNew:
|
||||
return nats.DeliverNewPolicy, nil
|
||||
default:
|
||||
return nats.DeliverAllPolicy, merrors.InvalidArgumentf("unsupported durable consumer deliver policy: %q", deliveryMode)
|
||||
}
|
||||
}
|
||||
|
||||
func natsDeliverPolicyName(deliveryMode nats.DeliverPolicy) string {
|
||||
switch deliveryMode {
|
||||
case nats.DeliverAllPolicy:
|
||||
return "all"
|
||||
case nats.DeliverLastPolicy:
|
||||
return "last"
|
||||
case nats.DeliverNewPolicy:
|
||||
return "new"
|
||||
case nats.DeliverByStartSequencePolicy:
|
||||
return "by_start_sequence"
|
||||
case nats.DeliverByStartTimePolicy:
|
||||
return "by_start_time"
|
||||
case nats.DeliverLastPerSubjectPolicy:
|
||||
return "last_per_subject"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func validateStreamConfig(current nats.StreamConfig, expected nats.StreamConfig, strict bool) error {
|
||||
if strings.TrimSpace(current.Name) != strings.TrimSpace(expected.Name) {
|
||||
return merrors.DataConflictf("durable stream name drift: got=%s want=%s", current.Name, expected.Name)
|
||||
}
|
||||
if !sameStrings(current.Subjects, expected.Subjects) {
|
||||
return merrors.DataConflictf("durable stream subjects drift: current=%v expected=%v", current.Subjects, expected.Subjects)
|
||||
}
|
||||
if current.Storage != expected.Storage {
|
||||
return merrors.DataConflict("durable stream storage drift")
|
||||
}
|
||||
if current.Retention != expected.Retention {
|
||||
return merrors.DataConflict("durable stream retention drift")
|
||||
}
|
||||
if current.Replicas != expected.Replicas {
|
||||
return merrors.DataConflict("durable stream replicas drift")
|
||||
}
|
||||
if strict && current.MaxAge != expected.MaxAge {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConsumerConfig(current nats.ConsumerConfig, expected nats.ConsumerConfig, strict bool) error {
|
||||
if strings.TrimSpace(current.Durable) != strings.TrimSpace(expected.Durable) {
|
||||
return merrors.DataConflict("durable consumer name drift")
|
||||
}
|
||||
if strings.TrimSpace(current.FilterSubject) != strings.TrimSpace(expected.FilterSubject) {
|
||||
return merrors.DataConflict("durable consumer filter subject drift")
|
||||
}
|
||||
if current.DeliverPolicy != expected.DeliverPolicy {
|
||||
return merrors.DataConflict("durable consumer deliver policy drift; delete and recreate consumer to apply deliver policy")
|
||||
}
|
||||
if current.AckPolicy != expected.AckPolicy {
|
||||
return merrors.DataConflict("durable consumer ack policy drift")
|
||||
}
|
||||
if strict && current.AckWait != expected.AckWait {
|
||||
return merrors.DataConflict("durable consumer ack wait drift")
|
||||
}
|
||||
if strict && current.MaxDeliver != expected.MaxDeliver {
|
||||
return merrors.DataConflict("durable consumer max deliver drift")
|
||||
}
|
||||
if strict && current.InactiveThreshold != expected.InactiveThreshold {
|
||||
return merrors.DataConflict("durable consumer inactive threshold drift")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameStrings(left, right []string) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
seen := map[string]int{}
|
||||
for _, value := range left {
|
||||
seen[strings.TrimSpace(value)]++
|
||||
}
|
||||
for _, value := range right {
|
||||
key := strings.TrimSpace(value)
|
||||
if seen[key] == 0 {
|
||||
return false
|
||||
}
|
||||
seen[key]--
|
||||
}
|
||||
for _, count := range seen {
|
||||
if count != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func stringSetDiff(left, right []string) []string {
|
||||
if len(left) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]int{}
|
||||
for _, value := range right {
|
||||
seen[strings.TrimSpace(value)]++
|
||||
}
|
||||
result := make([]string, 0)
|
||||
for _, value := range left {
|
||||
key := strings.TrimSpace(value)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if seen[key] > 0 {
|
||||
seen[key]--
|
||||
continue
|
||||
}
|
||||
result = append(result, key)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package natscap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/shared/messaging/capability"
|
||||
"git.sendico.io/tech/sxchnge/shared/messaging/reliable"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
func TestFetchPollDeadlineIsBounded(t *testing.T) {
|
||||
parent, cancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer cancel()
|
||||
ctx, stop, derived := fetchPollContext(parent, time.Second)
|
||||
if stop != nil {
|
||||
defer stop()
|
||||
}
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok || !derived || time.Until(deadline) > time.Second {
|
||||
t.Fatal("fetch escaped poll deadline")
|
||||
}
|
||||
}
|
||||
func TestClosedSubscriptionAndAck(t *testing.T) {
|
||||
s := &natsSubscription{}
|
||||
if err := s.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.Fetch(context.Background(), 1, time.Second); !errors.Is(err, capability.ErrClosed) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := &natsDelivery{sub: s}
|
||||
if err := d.Ack(context.Background()); !errors.Is(err, capability.ErrClosed) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d.acked = true
|
||||
if err := d.Ack(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
func TestConsumerDriftRejected(t *testing.T) {
|
||||
want := nats.ConsumerConfig{Durable: "email", FilterSubject: "notifications.email", AckPolicy: nats.AckExplicitPolicy, DeliverPolicy: nats.DeliverAllPolicy, AckWait: time.Minute}
|
||||
current := want
|
||||
current.FilterSubject = "other"
|
||||
if err := validateConsumerConfig(current, want, true); err == nil {
|
||||
t.Fatal("subject drift accepted")
|
||||
}
|
||||
current = want
|
||||
current.AckWait = time.Second
|
||||
if err := validateConsumerConfig(current, want, true); err == nil {
|
||||
t.Fatal("ack drift accepted")
|
||||
}
|
||||
if _, err := natsDeliverPolicy(reliable.DurableDeliverPolicy("invalid")); err == nil {
|
||||
t.Fatal("invalid deliver policy accepted")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user