package discovery import ( "strings" "sync" "time" ) const ( DefaultHealthIntervalSec = 10 DefaultHealthTimeoutSec = 30 ) type RegistryEntry struct { ID string `json:"id"` InstanceID string `bson:"instanceId" json:"instanceId"` Service string `json:"service"` Rail string `json:"rail,omitempty"` Network string `json:"network,omitempty"` Operations []string `json:"operations,omitempty"` Currencies []string `json:"currencies,omitempty"` Limits *Limits `json:"limits,omitempty"` InvokeURI string `json:"invokeURI,omitempty"` RoutingPriority int `json:"routingPriority,omitempty"` Version string `json:"version,omitempty"` Health HealthParams `json:"health,omitempty"` LastHeartbeat time.Time `json:"lastHeartbeat,omitempty"` Status string `json:"status,omitempty"` Healthy bool `json:"healthy,omitempty"` } type Registry struct { mu sync.RWMutex entries map[string]*RegistryEntry byID map[string]map[string]struct{} byInstance map[string]map[string]struct{} } type UpdateResult struct { Entry RegistryEntry IsNew bool WasHealthy bool BecameHealthy bool } func NewRegistry() *Registry { return &Registry{ entries: map[string]*RegistryEntry{}, byID: map[string]map[string]struct{}{}, byInstance: map[string]map[string]struct{}{}, } } func (r *Registry) UpsertFromAnnouncement(announce Announcement, now time.Time) UpdateResult { entry := registryEntryFromAnnouncement(normalizeAnnouncement(announce), now) key := registryEntryKey(entry) if key == "" { return UpdateResult{Entry: entry} } r.mu.Lock() defer r.mu.Unlock() existing, ok := r.entries[key] wasHealthy := false if ok && existing != nil { wasHealthy = existing.isHealthyAt(now) r.unindexEntry(key, existing) } entry.Healthy = entry.isHealthyAt(now) r.entries[key] = &entry r.indexEntry(key, &entry) return UpdateResult{ Entry: entry, IsNew: !ok, WasHealthy: wasHealthy, BecameHealthy: !wasHealthy && entry.Healthy, } } func (r *Registry) UpsertEntry(entry RegistryEntry, now time.Time) UpdateResult { entry = normalizeEntry(entry) key := registryEntryKey(entry) if key == "" { return UpdateResult{Entry: entry} } if entry.LastHeartbeat.IsZero() { entry.LastHeartbeat = now } if strings.TrimSpace(entry.Status) == "" { entry.Status = "ok" } r.mu.Lock() defer r.mu.Unlock() existing, ok := r.entries[key] wasHealthy := false if ok && existing != nil { wasHealthy = existing.isHealthyAt(now) r.unindexEntry(key, existing) } entry.Healthy = entry.isHealthyAt(now) r.entries[key] = &entry r.indexEntry(key, &entry) return UpdateResult{ Entry: entry, IsNew: !ok, WasHealthy: wasHealthy, BecameHealthy: !wasHealthy && entry.Healthy, } } func (r *Registry) UpdateHeartbeat(id string, instanceID string, status string, ts time.Time, now time.Time) []UpdateResult { id = strings.TrimSpace(id) instanceID = strings.TrimSpace(instanceID) if id == "" && instanceID == "" { return nil } if status == "" { status = "ok" } if ts.IsZero() { ts = now } r.mu.Lock() defer r.mu.Unlock() keys := keysFromIndex(r.byInstance[instanceID]) if len(keys) == 0 && id != "" { keys = keysFromIndex(r.byID[id]) } if len(keys) == 0 { return nil } results := make([]UpdateResult, 0, len(keys)) for _, key := range keys { entry := r.entries[key] if entry == nil { continue } if id != "" && entry.ID != id { continue } if instanceID != "" && entry.InstanceID != instanceID { continue } wasHealthy := entry.isHealthyAt(now) entry.Status = status entry.LastHeartbeat = ts entry.Healthy = entry.isHealthyAt(now) results = append(results, UpdateResult{ Entry: *entry, IsNew: false, WasHealthy: wasHealthy, BecameHealthy: !wasHealthy && entry.Healthy, }) } return results } func (r *Registry) Delete(key string) bool { key = strings.TrimSpace(key) if key == "" { return false } r.mu.Lock() defer r.mu.Unlock() entry, ok := r.entries[key] if !ok { return false } delete(r.entries, key) r.unindexEntry(key, entry) return true } func (r *Registry) List(now time.Time, onlyHealthy bool) []RegistryEntry { r.mu.Lock() defer r.mu.Unlock() result := make([]RegistryEntry, 0, len(r.entries)) for _, entry := range r.entries { if entry == nil { continue } entry.Healthy = entry.isHealthyAt(now) if onlyHealthy && !entry.Healthy { continue } cp := *entry result = append(result, cp) } return result } func registryEntryFromAnnouncement(announce Announcement, now time.Time) RegistryEntry { status := "ok" return RegistryEntry{ ID: strings.TrimSpace(announce.ID), InstanceID: strings.TrimSpace(announce.InstanceID), Service: strings.TrimSpace(announce.Service), Rail: strings.ToUpper(strings.TrimSpace(announce.Rail)), Network: strings.ToUpper(strings.TrimSpace(announce.Network)), Operations: cloneStrings(announce.Operations), Currencies: cloneStrings(announce.Currencies), Limits: cloneLimits(announce.Limits), InvokeURI: strings.TrimSpace(announce.InvokeURI), RoutingPriority: announce.RoutingPriority, Version: strings.TrimSpace(announce.Version), Health: normalizeHealth(announce.Health), LastHeartbeat: now, Status: status, } } func normalizeEntry(entry RegistryEntry) RegistryEntry { entry.ID = strings.TrimSpace(entry.ID) entry.InstanceID = strings.TrimSpace(entry.InstanceID) if entry.InstanceID == "" { entry.InstanceID = entry.ID } entry.Service = strings.TrimSpace(entry.Service) entry.Rail = strings.ToUpper(strings.TrimSpace(entry.Rail)) entry.Network = strings.ToUpper(strings.TrimSpace(entry.Network)) entry.Operations = normalizeStrings(entry.Operations, false) entry.Currencies = normalizeStrings(entry.Currencies, true) entry.InvokeURI = strings.TrimSpace(entry.InvokeURI) entry.Version = strings.TrimSpace(entry.Version) entry.Status = strings.TrimSpace(entry.Status) entry.Health = normalizeHealth(entry.Health) if entry.Limits != nil { entry.Limits = normalizeLimits(*entry.Limits) } return entry } func normalizeAnnouncement(announce Announcement) Announcement { announce.ID = strings.TrimSpace(announce.ID) announce.InstanceID = strings.TrimSpace(announce.InstanceID) if announce.InstanceID == "" { announce.InstanceID = announce.ID } announce.Service = strings.TrimSpace(announce.Service) announce.Rail = strings.ToUpper(strings.TrimSpace(announce.Rail)) announce.Network = strings.ToUpper(strings.TrimSpace(announce.Network)) announce.Operations = normalizeStrings(announce.Operations, false) announce.Currencies = normalizeStrings(announce.Currencies, true) announce.InvokeURI = strings.TrimSpace(announce.InvokeURI) announce.Version = strings.TrimSpace(announce.Version) announce.Health = normalizeHealth(announce.Health) if announce.Limits != nil { announce.Limits = normalizeLimits(*announce.Limits) } return announce } func normalizeHealth(h HealthParams) HealthParams { if h.IntervalSec <= 0 { h.IntervalSec = DefaultHealthIntervalSec } if h.TimeoutSec <= 0 { h.TimeoutSec = DefaultHealthTimeoutSec } if h.TimeoutSec < h.IntervalSec { h.TimeoutSec = h.IntervalSec * 2 } return h } func normalizeLimits(l Limits) *Limits { res := l if len(res.VolumeLimit) == 0 { res.VolumeLimit = nil } if len(res.VelocityLimit) == 0 { res.VelocityLimit = nil } return &res } func cloneLimits(src *Limits) *Limits { if src == nil { return nil } dst := *src if src.VolumeLimit != nil { dst.VolumeLimit = map[string]string{} for key, value := range src.VolumeLimit { if strings.TrimSpace(key) == "" { continue } dst.VolumeLimit[strings.TrimSpace(key)] = strings.TrimSpace(value) } } if src.VelocityLimit != nil { dst.VelocityLimit = map[string]int{} for key, value := range src.VelocityLimit { if strings.TrimSpace(key) == "" { continue } dst.VelocityLimit[strings.TrimSpace(key)] = value } } return &dst } func normalizeStrings(values []string, upper bool) []string { if len(values) == 0 { return nil } seen := map[string]bool{} result := make([]string, 0, len(values)) for _, value := range values { clean := strings.TrimSpace(value) if clean == "" { continue } if upper { clean = strings.ToUpper(clean) } if seen[clean] { continue } seen[clean] = true result = append(result, clean) } if len(result) == 0 { return nil } return result } func cloneStrings(values []string) []string { if len(values) == 0 { return nil } out := make([]string, len(values)) copy(out, values) return out } func (r *Registry) indexEntry(key string, entry *RegistryEntry) { if r == nil || entry == nil || key == "" { return } if entry.ID != "" { addIndex(r.byID, entry.ID, key) } if entry.InstanceID != "" { addIndex(r.byInstance, entry.InstanceID, key) } } func (r *Registry) unindexEntry(key string, entry *RegistryEntry) { if r == nil || entry == nil || key == "" { return } if entry.ID != "" { removeIndex(r.byID, entry.ID, key) } if entry.InstanceID != "" { removeIndex(r.byInstance, entry.InstanceID, key) } } func addIndex(index map[string]map[string]struct{}, id string, key string) { if id == "" || key == "" { return } set := index[id] if set == nil { set = map[string]struct{}{} index[id] = set } set[key] = struct{}{} } func removeIndex(index map[string]map[string]struct{}, id string, key string) { if id == "" || key == "" { return } set := index[id] if set == nil { return } delete(set, key) if len(set) == 0 { delete(index, id) } } func keysFromIndex(index map[string]struct{}) []string { if len(index) == 0 { return nil } keys := make([]string, 0, len(index)) for key := range index { keys = append(keys, key) } return keys } func (e *RegistryEntry) isHealthyAt(now time.Time) bool { if e == nil { return false } status := strings.ToLower(strings.TrimSpace(e.Status)) if status != "" && status != "ok" { return false } if e.LastHeartbeat.IsZero() { return false } timeout := time.Duration(e.Health.TimeoutSec) * time.Second if timeout <= 0 { timeout = time.Duration(DefaultHealthTimeoutSec) * time.Second } return now.Sub(e.LastHeartbeat) <= timeout }