unified gateway interface
This commit is contained in:
258
api/pkg/discovery/registry.go
Normal file
258
api/pkg/discovery/registry.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHealthIntervalSec = 10
|
||||
DefaultHealthTimeoutSec = 30
|
||||
)
|
||||
|
||||
type RegistryEntry struct {
|
||||
ID string `json:"id"`
|
||||
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
|
||||
}
|
||||
|
||||
type UpdateResult struct {
|
||||
Entry RegistryEntry
|
||||
IsNew bool
|
||||
WasHealthy bool
|
||||
BecameHealthy bool
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
entries: map[string]*RegistryEntry{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) UpsertFromAnnouncement(announce Announcement, now time.Time) UpdateResult {
|
||||
entry := registryEntryFromAnnouncement(normalizeAnnouncement(announce), now)
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
existing, ok := r.entries[entry.ID]
|
||||
wasHealthy := false
|
||||
if ok && existing != nil {
|
||||
wasHealthy = existing.isHealthyAt(now)
|
||||
}
|
||||
entry.Healthy = entry.isHealthyAt(now)
|
||||
r.entries[entry.ID] = &entry
|
||||
|
||||
return UpdateResult{
|
||||
Entry: entry,
|
||||
IsNew: !ok,
|
||||
WasHealthy: wasHealthy,
|
||||
BecameHealthy: !wasHealthy && entry.Healthy,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) UpdateHeartbeat(id string, status string, ts time.Time, now time.Time) (UpdateResult, bool) {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return UpdateResult{}, false
|
||||
}
|
||||
if status == "" {
|
||||
status = "ok"
|
||||
}
|
||||
if ts.IsZero() {
|
||||
ts = now
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
entry, ok := r.entries[id]
|
||||
if !ok || entry == nil {
|
||||
return UpdateResult{}, false
|
||||
}
|
||||
wasHealthy := entry.isHealthyAt(now)
|
||||
entry.Status = status
|
||||
entry.LastHeartbeat = ts
|
||||
entry.Healthy = entry.isHealthyAt(now)
|
||||
|
||||
return UpdateResult{
|
||||
Entry: *entry,
|
||||
IsNew: false,
|
||||
WasHealthy: wasHealthy,
|
||||
BecameHealthy: !wasHealthy && entry.Healthy,
|
||||
}, 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),
|
||||
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 normalizeAnnouncement(announce Announcement) Announcement {
|
||||
announce.ID = strings.TrimSpace(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 (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
|
||||
}
|
||||
Reference in New Issue
Block a user