Files
sendico/api/pkg/discovery/announcer.go
2026-01-04 12:57:40 +01:00

158 lines
3.3 KiB
Go

package discovery
import (
"os"
"strings"
"sync"
"time"
"github.com/google/uuid"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
)
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 = logger.Named("discovery")
}
announce = normalizeAnnouncement(announce)
if announce.Service == "" {
announce.Service = strings.TrimSpace(sender)
}
if announce.ID == "" {
announce.ID = DefaultInstanceID(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")
close(a.doneCh)
return
}
if strings.TrimSpace(a.announce.ID) == "" {
a.logWarn("Discovery announce skipped: missing instance id")
close(a.doneCh)
return
}
a.sendAnnouncement()
a.sendHeartbeat()
go a.heartbeatLoop()
})
}
func (a *Announcer) Stop() {
if a == nil {
return
}
a.stopOnce.Do(func() {
close(a.stopCh)
<-a.doneCh
})
}
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)
if a.announce.Rail != "" {
env = NewGatewayAnnounceEnvelope(a.sender, a.announce)
}
if err := a.producer.SendMessage(env); err != nil {
a.logWarn("Failed to publish discovery announce: " + err.Error())
return
}
a.logInfo("Discovery announce published")
}
func (a *Announcer) sendHeartbeat() {
hb := Heartbeat{
ID: a.announce.ID,
Status: "ok",
TS: time.Now().Unix(),
}
if err := a.producer.SendMessage(NewHeartbeatEnvelope(a.sender, hb)); err != nil {
a.logWarn("Failed to publish discovery heartbeat: " + err.Error())
}
}
func (a *Announcer) logInfo(message string) {
if a.logger == nil {
return
}
a.logger.Info(message)
}
func (a *Announcer) logWarn(message string) {
if a.logger == nil {
return
}
a.logger.Warn(message)
}
func DefaultInstanceID(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 DefaultInvokeURI(service string) string {
clean := strings.ToLower(strings.TrimSpace(service))
if clean == "" {
return ""
}
return "grpc://" + clean
}