2025-11-18 08:38:03 +08:00
|
|
|
package nats
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-19 16:49:53 +08:00
|
|
|
"errors"
|
2025-11-18 08:38:03 +08:00
|
|
|
"fmt"
|
2025-11-19 16:49:53 +08:00
|
|
|
"time"
|
2025-11-18 08:38:03 +08:00
|
|
|
|
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// StreamManager handles JetStream stream lifecycle management
|
|
|
|
|
type StreamManager struct {
|
|
|
|
|
js nats.JetStreamContext
|
|
|
|
|
streamName string
|
|
|
|
|
subjects []string
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 16:49:53 +08:00
|
|
|
// StreamConfig holds configuration for creating a JetStream stream
|
|
|
|
|
type StreamConfig struct {
|
|
|
|
|
MaxMsgs int64
|
|
|
|
|
MaxBytes int64
|
|
|
|
|
MaxAge time.Duration
|
|
|
|
|
Discard string // "old" or "new"
|
|
|
|
|
Storage string // "file" or "memory"
|
|
|
|
|
Replicas int
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 08:38:03 +08:00
|
|
|
// NewStreamManager creates a new stream manager
|
|
|
|
|
func NewStreamManager(js nats.JetStreamContext, streamName string, subjects []string, logger *zap.Logger) *StreamManager {
|
2025-12-24 14:53:14 +08:00
|
|
|
if logger == nil {
|
|
|
|
|
logger = zap.NewNop()
|
|
|
|
|
}
|
2025-11-18 08:38:03 +08:00
|
|
|
return &StreamManager{
|
|
|
|
|
js: js,
|
|
|
|
|
streamName: streamName,
|
|
|
|
|
subjects: subjects,
|
|
|
|
|
logger: logger,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 16:49:53 +08:00
|
|
|
// EnsureStream ensures that the configured JetStream stream exists, creating it if necessary
|
|
|
|
|
func (sm *StreamManager) EnsureStream(cfg *StreamConfig) error {
|
|
|
|
|
// Check if stream already exists
|
2025-11-19 17:09:52 +08:00
|
|
|
info, err := sm.js.StreamInfo(sm.streamName)
|
2025-11-19 16:49:53 +08:00
|
|
|
if err == nil {
|
2025-11-19 17:09:52 +08:00
|
|
|
// Stream exists - check if we need to add any missing subjects
|
|
|
|
|
existingSubjects := make(map[string]bool)
|
|
|
|
|
for _, subj := range info.Config.Subjects {
|
|
|
|
|
existingSubjects[subj] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if any configured subjects are missing
|
|
|
|
|
missingSubjects := []string{}
|
|
|
|
|
for _, subj := range sm.subjects {
|
|
|
|
|
if !existingSubjects[subj] {
|
|
|
|
|
missingSubjects = append(missingSubjects, subj)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(missingSubjects) > 0 {
|
|
|
|
|
// Update stream to include missing subjects
|
|
|
|
|
updatedSubjects := info.Config.Subjects
|
|
|
|
|
updatedSubjects = append(updatedSubjects, missingSubjects...)
|
|
|
|
|
info.Config.Subjects = updatedSubjects
|
|
|
|
|
|
|
|
|
|
_, updateErr := sm.js.UpdateStream(&info.Config)
|
|
|
|
|
if updateErr != nil {
|
|
|
|
|
return fmt.Errorf("failed to update stream %s with new subjects %v: %w", sm.streamName, missingSubjects, updateErr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sm.logger.Info("Updated JetStream stream with new subjects",
|
|
|
|
|
zap.String("stream", sm.streamName),
|
|
|
|
|
zap.Strings("added_subjects", missingSubjects),
|
|
|
|
|
zap.Strings("all_subjects", updatedSubjects),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
sm.logger.Info("JetStream stream verified",
|
|
|
|
|
zap.String("stream", sm.streamName),
|
|
|
|
|
zap.Strings("subjects", sm.subjects),
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-11-19 16:49:53 +08:00
|
|
|
return nil
|
2025-11-18 08:38:03 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-19 16:49:53 +08:00
|
|
|
// If stream doesn't exist, create it
|
|
|
|
|
if errors.Is(err, nats.ErrStreamNotFound) {
|
|
|
|
|
streamCfg := &nats.StreamConfig{
|
|
|
|
|
Name: sm.streamName,
|
|
|
|
|
Subjects: sm.subjects,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply limits if provided
|
|
|
|
|
if cfg != nil {
|
|
|
|
|
if cfg.MaxMsgs > 0 {
|
|
|
|
|
streamCfg.MaxMsgs = cfg.MaxMsgs
|
|
|
|
|
}
|
|
|
|
|
if cfg.MaxBytes > 0 {
|
|
|
|
|
streamCfg.MaxBytes = cfg.MaxBytes
|
|
|
|
|
}
|
|
|
|
|
if cfg.MaxAge > 0 {
|
|
|
|
|
streamCfg.MaxAge = cfg.MaxAge
|
|
|
|
|
}
|
|
|
|
|
if cfg.Discard == "new" {
|
|
|
|
|
streamCfg.Discard = nats.DiscardNew
|
|
|
|
|
} else {
|
|
|
|
|
streamCfg.Discard = nats.DiscardOld
|
|
|
|
|
}
|
|
|
|
|
if cfg.Storage == "memory" {
|
|
|
|
|
streamCfg.Storage = nats.MemoryStorage
|
|
|
|
|
} else {
|
|
|
|
|
streamCfg.Storage = nats.FileStorage
|
|
|
|
|
}
|
|
|
|
|
if cfg.Replicas > 0 {
|
|
|
|
|
streamCfg.Replicas = cfg.Replicas
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err := sm.js.AddStream(streamCfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create stream %s: %w", sm.streamName, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sm.logger.Info("Created JetStream stream",
|
|
|
|
|
zap.String("stream", sm.streamName),
|
|
|
|
|
zap.Strings("subjects", sm.subjects),
|
|
|
|
|
)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Other error (e.g., permission denied)
|
|
|
|
|
return fmt.Errorf("stream %s not found or inaccessible: %w", sm.streamName, err)
|
2025-11-18 08:38:03 +08:00
|
|
|
}
|