package discovery import ( "os" "strings" "sync" "time" "github.com/google/uuid" msg "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" "go.uber.org/zap" ) type Announcer struct { logger mlogger.Logger producer msg.Producer sender string announce Announcement startOnce sync.Once stopOnce sync.Once stopCh chan struct{} doneCh chan struct{} } func NewAnnouncer(logger mlogger.Logger, producer msg.Producer, sender string, announce Announcement) *Announcer { if logger == nil { logger = zap.NewNop() } logger = logger.Named("discovery") announce = normalizeAnnouncement(announce) if announce.Service == "" { announce.Service = strings.TrimSpace(sender) } if announce.InstanceID == "" { announce.InstanceID = InstanceID() } if announce.ID == "" { announce.ID = DefaultEntryID(announce.Service) } if announce.InvokeURI == "" && announce.Service != "" { announce.InvokeURI = DefaultInvokeURI(announce.Service) } return &Announcer{ logger: logger, producer: producer, sender: strings.TrimSpace(sender), announce: announce, stopCh: make(chan struct{}), doneCh: make(chan struct{}), } } func (a *Announcer) Start() { if a == nil { return } a.startOnce.Do(func() { if a.producer == nil { a.logWarn("Discovery announce skipped: producer not configured", announcementFields(a.announce)...) close(a.doneCh) return } if strings.TrimSpace(a.announce.ID) == "" { a.logWarn("Discovery announce skipped: missing instance id", announcementFields(a.announce)...) close(a.doneCh) return } a.logInfo("Discovery announcer starting", announcementFields(a.announce)...) a.sendAnnouncement() a.sendHeartbeat() go a.heartbeatLoop() }) } func (a *Announcer) Stop() { if a == nil { return } a.stopOnce.Do(func() { close(a.stopCh) <-a.doneCh a.logInfo("Discovery announcer stopped", announcementFields(a.announce)...) }) } func (a *Announcer) heartbeatLoop() { defer close(a.doneCh) interval := time.Duration(a.announce.Health.IntervalSec) * time.Second if interval <= 0 { interval = time.Duration(DefaultHealthIntervalSec) * time.Second } ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-a.stopCh: return case <-ticker.C: a.sendHeartbeat() } } } func (a *Announcer) sendAnnouncement() { env := NewServiceAnnounceEnvelope(a.sender, a.announce) event := ServiceAnnounceEvent() if a.announce.Rail != "" { env = NewGatewayAnnounceEnvelope(a.sender, a.announce) event = GatewayAnnounceEvent() } if err := a.producer.SendMessage(env); err != nil { fields := append(announcementFields(a.announce), zap.String("event", event.ToString()), zap.Error(err)) a.logWarn("Failed to publish discovery announce", fields...) return } a.logInfo("Discovery announce published", append(announcementFields(a.announce), zap.String("event", event.ToString()))...) } func (a *Announcer) sendHeartbeat() { hb := Heartbeat{ ID: a.announce.ID, InstanceID: a.announce.InstanceID, Status: "ok", TS: time.Now().Unix(), } if err := a.producer.SendMessage(NewHeartbeatEnvelope(a.sender, hb)); err != nil { fields := append(announcementFields(a.announce), zap.String("event", HeartbeatEvent().ToString()), zap.Error(err)) a.logWarn("Failed to publish discovery heartbeat", fields...) } } func (a *Announcer) logInfo(message string, fields ...zap.Field) { if a == nil { return } a.logger.Info(message, fields...) } func (a *Announcer) logWarn(message string, fields ...zap.Field) { if a == nil { return } a.logger.Warn(message, fields...) } func DefaultEntryID(service string) string { clean := strings.ToLower(strings.TrimSpace(service)) if clean == "" { clean = "service" } host, _ := os.Hostname() host = strings.ToLower(strings.TrimSpace(host)) uid := uuid.NewString() if host == "" { return clean + "_" + uid } return clean + "_" + host + "_" + uid } func DefaultInstanceID(service string) string { return DefaultEntryID(service) } func DefaultInvokeURI(service string) string { clean := strings.ToLower(strings.TrimSpace(service)) if clean == "" { return "" } return "grpc://" + clean }