package discovery import ( "encoding/json" "sync" "time" "github.com/nats-io/nats.go" mb "github.com/tech/sendico/pkg/messaging/broker" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "go.uber.org/zap" ) type RegistryWatcher struct { logger mlogger.Logger registry *Registry kv *KVStore watcher nats.KeyWatcher stopOnce sync.Once } func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Registry) (*RegistryWatcher, error) { if msgBroker == nil { return nil, merrors.InvalidArgument("discovery watcher: broker is nil") } if registry == nil { registry = NewRegistry() } if logger == nil { return nil, merrors.InvalidArgument("discovery logger: logger must be provided") } logger = logger.Named("discovery_watcher") provider, ok := msgBroker.(jetStreamProvider) if !ok { return nil, merrors.Internal("discovery watcher: jetstream not available") } js := provider.JetStream() if js == nil { return nil, merrors.Internal("discovery watcher: jetstream not configured") } store, err := NewKVStore(logger, js, "") if err != nil { return nil, err } return &RegistryWatcher{ logger: logger, registry: registry, kv: store, }, nil } func (w *RegistryWatcher) Start() error { if w == nil || w.kv == nil { return merrors.Internal("discovery watcher: not configured") } watcher, err := w.kv.WatchAll() if err != nil { return err } w.watcher = watcher w.logger.Info("Discovery registry watcher started", zap.String("bucket", w.kv.Bucket())) go w.consume(watcher) return nil } func (w *RegistryWatcher) Stop() { if w == nil { return } w.stopOnce.Do(func() { if w.watcher != nil { _ = w.watcher.Stop() } w.logger.Info("Discovery registry watcher stopped") }) } func (w *RegistryWatcher) Registry() *Registry { if w == nil { return nil } return w.registry } func (w *RegistryWatcher) consume(watcher nats.KeyWatcher) { if w == nil || watcher == nil { return } initial := true initialCount := 0 for entry := range watcher.Updates() { if entry == nil { if initial { fields := []zap.Field{zap.Int("entries", initialCount)} if w.kv != nil { fields = append(fields, zap.String("bucket", w.kv.Bucket())) } w.logger.Info("Discovery registry watcher initial sync complete", fields...) initial = false } continue } if initial && entry.Operation() == nats.KeyValuePut { initialCount++ } switch entry.Operation() { case nats.KeyValueDelete, nats.KeyValuePurge: key := registryKeyFromKVKey(entry.Key()) if key != "" { if w.registry.Delete(key) { w.logger.Info("Discovery registry entry removed", zap.String("key", key)) } } continue case nats.KeyValuePut: default: continue } var payload RegistryEntry if err := json.Unmarshal(entry.Value(), &payload); err != nil { w.logger.Warn("Failed to decode discovery KV entry", zap.String("key", entry.Key()), zap.Error(err)) continue } result := w.registry.UpsertEntry(payload, time.Now()) if result.IsNew || result.BecameHealthy { fields := append(entryFields(result.Entry), zap.Bool("is_new", result.IsNew), zap.Bool("became_healthy", result.BecameHealthy)) w.logger.Info("Discovery registry entry updated from KV", fields...) } } }