Complete notification expiry and configurable durable delivery (#36)

This commit is contained in:
Stephan D committed 2026-09-12 14:52:44 +02:00
1 parent 326d58f705
commit 4b14649b3a
16 files changed
+320 -40

No files matched your search

@@ -88,22 +88,22 @@ func (a *DurableAdapter) OpenConsumer(ctx context.Context, settings reliable.Dur
return &natsSubscription{sub: sub}, nil
}
func (a *DurableAdapter) EnsureProducerResources(_ context.Context, settings reliable.DurableSettings) error {
func (a *DurableAdapter) EnsureProducerResources(ctx 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)
return ensureDurableStream(ctx, a.broker.Logger(), a.jetStream(), settings)
}
func (a *DurableAdapter) EnsureConsumerResources(_ context.Context, settings reliable.DurableSettings, consumer reliable.DurableConsumerSettings) error {
func (a *DurableAdapter) EnsureConsumerResources(ctx 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 {
if err := ensureDurableStream(ctx, a.broker.Logger(), js, settings); err != nil {
return err
}
return ensureDurableConsumerConfig(a.broker.Logger(), js, settings, consumer)
return ensureDurableConsumerConfig(ctx, a.broker.Logger(), js, settings, consumer)
}
func (a *DurableAdapter) jetStream() nats.JetStreamContext {
@@ -1,6 +1,7 @@
package natscap
import (
"context"
"errors"
"strings"
"time"
@@ -12,7 +13,7 @@ import (
"go.uber.org/zap"
)
func ensureDurableStream(logger *zap.Logger, js durableAdminContext, settings reliable.DurableSettings) error {
func ensureDurableStream(ctx context.Context, logger *zap.Logger, js durableAdminContext, settings reliable.DurableSettings) error {
if js == nil {
return capability.Unavailable("durable admin")
}
@@ -29,7 +30,7 @@ func ensureDurableStream(logger *zap.Logger, js durableAdminContext, settings re
expected.Storage = nats.FileStorage
}
info, err := js.StreamInfo(expected.Name)
info, err := js.StreamInfo(expected.Name, nats.Context(ctx))
if err != nil {
if errors.Is(err, nats.ErrStreamNotFound) {
if !settings.AllowsAutoProvision() {
@@ -38,15 +39,15 @@ func ensureDurableStream(logger *zap.Logger, js durableAdminContext, settings re
}
return err
}
_, err = js.AddStream(expected)
_, err = js.AddStream(expected, nats.Context(ctx))
return err
}
return err
}
current := info.Config
if err := validateStreamConfig(current, *expected, settings.IsProductionValidation()); err != nil {
if err := validateStreamConfig(current, *expected, settings.IsProductionValidation() || !settings.AllowsAutoProvision()); err != nil {
if settings.AllowsAutoProvision() {
reconciled, reconcileErr := reconcileDurableStream(logger, js, current, *expected, settings)
reconciled, reconcileErr := reconcileDurableStream(ctx, logger, js, current, *expected, settings)
if reconciled {
return reconcileErr
}
@@ -58,13 +59,13 @@ func ensureDurableStream(logger *zap.Logger, js durableAdminContext, settings re
}
if settings.AllowsAutoProvision() && current.MaxAge != expected.MaxAge {
current.MaxAge = expected.MaxAge
_, err = js.UpdateStream(&current)
_, err = js.UpdateStream(&current, nats.Context(ctx))
return err
}
return nil
}
func reconcileDurableStream(logger *zap.Logger, js durableAdminContext, current nats.StreamConfig, expected nats.StreamConfig, settings reliable.DurableSettings) (bool, error) {
func reconcileDurableStream(ctx context.Context, 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")
}
@@ -90,14 +91,14 @@ func reconcileDurableStream(logger *zap.Logger, js durableAdminContext, current
}
current.Subjects = append([]string(nil), expected.Subjects...)
current.MaxAge = expected.MaxAge
_, err := js.UpdateStream(&current)
_, err := js.UpdateStream(&current, nats.Context(ctx))
if err != nil {
return true, err
}
return true, nil
}
func ensureDurableConsumerConfig(logger *zap.Logger, js durableAdminContext, settings reliable.DurableSettings, consumer reliable.DurableConsumerSettings) error {
func ensureDurableConsumerConfig(ctx context.Context, logger *zap.Logger, js durableAdminContext, settings reliable.DurableSettings, consumer reliable.DurableConsumerSettings) error {
if js == nil {
return capability.Unavailable("durable admin")
}
@@ -117,7 +118,7 @@ func ensureDurableConsumerConfig(logger *zap.Logger, js durableAdminContext, set
if consumer.InactiveThresholdSeconds > 0 {
expected.InactiveThreshold = time.Duration(consumer.InactiveThresholdSeconds) * time.Second
}
info, err := js.ConsumerInfo(settings.Stream.Name, consumer.Durable)
info, err := js.ConsumerInfo(settings.Stream.Name, consumer.Durable, nats.Context(ctx))
if err != nil {
if errors.Is(err, nats.ErrConsumerNotFound) {
if !settings.AllowsAutoProvision() {
@@ -126,7 +127,7 @@ func ensureDurableConsumerConfig(logger *zap.Logger, js durableAdminContext, set
}
return err
}
_, err = js.AddConsumer(settings.Stream.Name, expected)
_, err = js.AddConsumer(settings.Stream.Name, expected, nats.Context(ctx))
return err
}
return err
@@ -142,14 +143,14 @@ func ensureDurableConsumerConfig(logger *zap.Logger, js durableAdminContext, set
zap.Bool("recreate_required", true),
)
}
if err := validateConsumerConfig(current, *expected, settings.IsProductionValidation()); err != nil {
if err := validateConsumerConfig(current, *expected, settings.IsProductionValidation() || !settings.AllowsAutoProvision()); 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, &current)
_, err = js.UpdateConsumer(settings.Stream.Name, &current, nats.Context(ctx))
return err
}
return nil
@@ -202,7 +203,7 @@ func validateStreamConfig(current nats.StreamConfig, expected nats.StreamConfig,
return merrors.DataConflict("durable stream replicas drift")
}
if strict && current.MaxAge != expected.MaxAge {
return nil
return merrors.DataConflict("durable stream retention age drift")
}
return nil
}
@@ -56,3 +56,15 @@ func TestConsumerDriftRejected(t *testing.T) {
t.Fatal("invalid deliver policy accepted")
}
}
func TestProductionStreamRetentionDriftRejected(t *testing.T) {
expected := nats.StreamConfig{Name: "NOTIFICATIONS", Subjects: []string{"notifications.email"}, Storage: nats.FileStorage, Retention: nats.LimitsPolicy, Replicas: 3, MaxAge: 48 * time.Hour}
current := expected
current.MaxAge = time.Hour
if err := validateStreamConfig(current, expected, true); err == nil {
t.Fatal("production retention drift accepted")
}
if err := validateStreamConfig(current, expected, false); err != nil {
t.Fatal("development retention cannot be reconciled", err)
}
}