792 lines
19 KiB
Go
792 lines
19 KiB
Go
package discovery
|
|
|
|
import (
|
|
"fmt"
|
|
"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"`
|
|
CurrencyMeta []CurrencyAnnouncement `json:"currencyMeta,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"
|
|
currencies := cloneCurrencyAnnouncements(announce.Currencies)
|
|
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: legacyNetworkFromCurrencies(currencies),
|
|
Operations: cloneStrings(announce.Operations),
|
|
Currencies: legacyCurrencyCodes(currencies),
|
|
CurrencyMeta: currencies,
|
|
Limits: legacyLimitsFromCurrencies(currencies),
|
|
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 = NormalizeRail(entry.Rail)
|
|
entry.Network = strings.ToUpper(strings.TrimSpace(entry.Network))
|
|
entry.Operations = normalizeStrings(entry.Operations, false)
|
|
entry.CurrencyMeta = normalizeCurrencyAnnouncements(entry.CurrencyMeta)
|
|
if len(entry.CurrencyMeta) > 0 {
|
|
entry.Currencies = legacyCurrencyCodes(entry.CurrencyMeta)
|
|
if derivedNetwork := legacyNetworkFromCurrencies(entry.CurrencyMeta); derivedNetwork != "" {
|
|
entry.Network = derivedNetwork
|
|
}
|
|
entry.Limits = legacyLimitsFromCurrencies(entry.CurrencyMeta)
|
|
} else {
|
|
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 len(entry.CurrencyMeta) == 0 && 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 = NormalizeRail(announce.Rail)
|
|
announce.Operations = normalizeStrings(announce.Operations, false)
|
|
announce.Currencies = normalizeCurrencyAnnouncements(announce.Currencies)
|
|
announce.InvokeURI = strings.TrimSpace(announce.InvokeURI)
|
|
announce.Version = strings.TrimSpace(announce.Version)
|
|
announce.Health = normalizeHealth(announce.Health)
|
|
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 normalizeCurrencyAnnouncements(values []CurrencyAnnouncement) []CurrencyAnnouncement {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
seen := map[string]bool{}
|
|
result := make([]CurrencyAnnouncement, 0, len(values))
|
|
for _, value := range values {
|
|
clean, ok := normalizeCurrencyAnnouncement(value)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key := strings.Join([]string{
|
|
clean.Currency,
|
|
clean.Network,
|
|
clean.ProviderID,
|
|
clean.ContractAddress,
|
|
}, "|")
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
result = append(result, clean)
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func normalizeCurrencyAnnouncement(src CurrencyAnnouncement) (CurrencyAnnouncement, bool) {
|
|
src.Currency = strings.ToUpper(strings.TrimSpace(src.Currency))
|
|
if src.Currency == "" {
|
|
return CurrencyAnnouncement{}, false
|
|
}
|
|
src.Network = strings.ToUpper(strings.TrimSpace(src.Network))
|
|
src.ProviderID = strings.TrimSpace(src.ProviderID)
|
|
src.ContractAddress = strings.ToLower(strings.TrimSpace(src.ContractAddress))
|
|
if src.Decimals != nil && *src.Decimals < 0 {
|
|
src.Decimals = nil
|
|
}
|
|
src.Limits = normalizeCurrencyLimits(src.Limits)
|
|
return src, true
|
|
}
|
|
|
|
func normalizeCurrencyLimits(src *CurrencyLimits) *CurrencyLimits {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
dst := &CurrencyLimits{}
|
|
if src.Amount != nil {
|
|
amount := &CurrencyAmount{
|
|
Min: strings.TrimSpace(src.Amount.Min),
|
|
Max: strings.TrimSpace(src.Amount.Max),
|
|
}
|
|
if amount.Min != "" || amount.Max != "" {
|
|
dst.Amount = amount
|
|
}
|
|
}
|
|
if src.Running != nil {
|
|
running := &CurrencyRunningLimits{}
|
|
for _, limit := range src.Running.Volume {
|
|
max := strings.TrimSpace(limit.Max)
|
|
if max == "" {
|
|
continue
|
|
}
|
|
window := normalizeWindow(limit.Window)
|
|
if legacyWindowKey(window) == "" {
|
|
continue
|
|
}
|
|
running.Volume = append(running.Volume, VolumeLimit{
|
|
Window: window,
|
|
Max: max,
|
|
})
|
|
}
|
|
for _, limit := range src.Running.Velocity {
|
|
if limit.Max <= 0 {
|
|
continue
|
|
}
|
|
window := normalizeWindow(limit.Window)
|
|
if legacyWindowKey(window) == "" {
|
|
continue
|
|
}
|
|
running.Velocity = append(running.Velocity, VelocityLimit{
|
|
Window: window,
|
|
Max: limit.Max,
|
|
})
|
|
}
|
|
if len(running.Volume) > 0 || len(running.Velocity) > 0 {
|
|
dst.Running = running
|
|
}
|
|
}
|
|
if dst.Amount == nil && dst.Running == nil {
|
|
return nil
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func normalizeWindow(src Window) Window {
|
|
src.Raw = strings.TrimSpace(src.Raw)
|
|
src.Duration = strings.TrimSpace(src.Duration)
|
|
src.Named = strings.TrimSpace(src.Named)
|
|
if src.Calendar != nil {
|
|
cal := &CalendarWindow{
|
|
Unit: CalendarUnit(strings.ToLower(strings.TrimSpace(string(src.Calendar.Unit)))),
|
|
Count: src.Calendar.Count,
|
|
}
|
|
if cal.Count <= 0 {
|
|
cal.Count = 1
|
|
}
|
|
if cal.Unit == CalendarUnitUnspecified {
|
|
cal = nil
|
|
}
|
|
src.Calendar = cal
|
|
}
|
|
return src
|
|
}
|
|
|
|
func cloneCurrencyAnnouncements(values []CurrencyAnnouncement) []CurrencyAnnouncement {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]CurrencyAnnouncement, 0, len(values))
|
|
for _, value := range values {
|
|
cp := CurrencyAnnouncement{
|
|
Currency: value.Currency,
|
|
Network: value.Network,
|
|
ProviderID: value.ProviderID,
|
|
ContractAddress: value.ContractAddress,
|
|
}
|
|
if value.Decimals != nil {
|
|
decimals := *value.Decimals
|
|
cp.Decimals = &decimals
|
|
}
|
|
cp.Limits = cloneCurrencyLimits(value.Limits)
|
|
out = append(out, cp)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneCurrencyLimits(src *CurrencyLimits) *CurrencyLimits {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
dst := &CurrencyLimits{}
|
|
if src.Amount != nil {
|
|
dst.Amount = &CurrencyAmount{
|
|
Min: src.Amount.Min,
|
|
Max: src.Amount.Max,
|
|
}
|
|
}
|
|
if src.Running != nil {
|
|
running := &CurrencyRunningLimits{}
|
|
if len(src.Running.Volume) > 0 {
|
|
running.Volume = make([]VolumeLimit, 0, len(src.Running.Volume))
|
|
for _, item := range src.Running.Volume {
|
|
running.Volume = append(running.Volume, VolumeLimit{
|
|
Window: cloneWindow(item.Window),
|
|
Max: item.Max,
|
|
})
|
|
}
|
|
}
|
|
if len(src.Running.Velocity) > 0 {
|
|
running.Velocity = make([]VelocityLimit, 0, len(src.Running.Velocity))
|
|
for _, item := range src.Running.Velocity {
|
|
running.Velocity = append(running.Velocity, VelocityLimit{
|
|
Window: cloneWindow(item.Window),
|
|
Max: item.Max,
|
|
})
|
|
}
|
|
}
|
|
if len(running.Volume) > 0 || len(running.Velocity) > 0 {
|
|
dst.Running = running
|
|
}
|
|
}
|
|
if dst.Amount == nil && dst.Running == nil {
|
|
return nil
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func cloneWindow(src Window) Window {
|
|
dst := Window{
|
|
Raw: src.Raw,
|
|
Duration: src.Duration,
|
|
Named: src.Named,
|
|
}
|
|
if src.Calendar != nil {
|
|
dst.Calendar = &CalendarWindow{
|
|
Unit: src.Calendar.Unit,
|
|
Count: src.Calendar.Count,
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func legacyCurrencyCodes(values []CurrencyAnnouncement) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
seen := map[string]bool{}
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
currency := strings.ToUpper(strings.TrimSpace(value.Currency))
|
|
if currency == "" || seen[currency] {
|
|
continue
|
|
}
|
|
seen[currency] = true
|
|
out = append(out, currency)
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func legacyNetworkFromCurrencies(values []CurrencyAnnouncement) string {
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
network := ""
|
|
for _, value := range values {
|
|
current := strings.ToUpper(strings.TrimSpace(value.Network))
|
|
if current == "" {
|
|
continue
|
|
}
|
|
if network == "" {
|
|
network = current
|
|
continue
|
|
}
|
|
if network != current {
|
|
return ""
|
|
}
|
|
}
|
|
return network
|
|
}
|
|
|
|
func legacyLimitsFromCurrencies(values []CurrencyAnnouncement) *Limits {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
var merged *Limits
|
|
for _, value := range values {
|
|
current := legacyLimitsFromCurrency(value.Limits)
|
|
if current == nil {
|
|
continue
|
|
}
|
|
if merged == nil {
|
|
merged = current
|
|
continue
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(merged.MinAmount), strings.TrimSpace(current.MinAmount)) {
|
|
merged.MinAmount = ""
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(merged.MaxAmount), strings.TrimSpace(current.MaxAmount)) {
|
|
merged.MaxAmount = ""
|
|
}
|
|
merged.VolumeLimit = intersectStringMaps(merged.VolumeLimit, current.VolumeLimit)
|
|
merged.VelocityLimit = intersectIntMaps(merged.VelocityLimit, current.VelocityLimit)
|
|
}
|
|
if merged == nil {
|
|
return nil
|
|
}
|
|
if merged.MinAmount == "" && merged.MaxAmount == "" && len(merged.VolumeLimit) == 0 && len(merged.VelocityLimit) == 0 {
|
|
return nil
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func legacyLimitsFromCurrency(src *CurrencyLimits) *Limits {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
out := &Limits{}
|
|
if src.Amount != nil {
|
|
out.MinAmount = strings.TrimSpace(src.Amount.Min)
|
|
out.MaxAmount = strings.TrimSpace(src.Amount.Max)
|
|
}
|
|
if src.Running != nil {
|
|
if len(src.Running.Volume) > 0 {
|
|
out.VolumeLimit = map[string]string{}
|
|
for _, item := range src.Running.Volume {
|
|
key := legacyWindowKey(item.Window)
|
|
max := strings.TrimSpace(item.Max)
|
|
if key == "" || max == "" {
|
|
continue
|
|
}
|
|
out.VolumeLimit[key] = max
|
|
}
|
|
}
|
|
if len(src.Running.Velocity) > 0 {
|
|
out.VelocityLimit = map[string]int{}
|
|
for _, item := range src.Running.Velocity {
|
|
key := legacyWindowKey(item.Window)
|
|
if key == "" || item.Max <= 0 {
|
|
continue
|
|
}
|
|
out.VelocityLimit[key] = item.Max
|
|
}
|
|
}
|
|
}
|
|
if out.MinAmount == "" && out.MaxAmount == "" && len(out.VolumeLimit) == 0 && len(out.VelocityLimit) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func intersectStringMaps(left, right map[string]string) map[string]string {
|
|
if len(left) == 0 || len(right) == 0 {
|
|
return nil
|
|
}
|
|
out := map[string]string{}
|
|
for key, value := range left {
|
|
if rightValue, ok := right[key]; ok && strings.EqualFold(strings.TrimSpace(value), strings.TrimSpace(rightValue)) {
|
|
out[key] = value
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func intersectIntMaps(left, right map[string]int) map[string]int {
|
|
if len(left) == 0 || len(right) == 0 {
|
|
return nil
|
|
}
|
|
out := map[string]int{}
|
|
for key, value := range left {
|
|
if rightValue, ok := right[key]; ok && value == rightValue {
|
|
out[key] = value
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func legacyWindowKey(window Window) string {
|
|
if raw := strings.TrimSpace(window.Raw); raw != "" {
|
|
return raw
|
|
}
|
|
if named := strings.TrimSpace(window.Named); named != "" {
|
|
return named
|
|
}
|
|
if duration := strings.TrimSpace(window.Duration); duration != "" {
|
|
return duration
|
|
}
|
|
if window.Calendar != nil && window.Calendar.Unit != CalendarUnitUnspecified {
|
|
count := window.Calendar.Count
|
|
if count <= 0 {
|
|
count = 1
|
|
}
|
|
return fmt.Sprintf("%d%s", count, strings.ToLower(strings.TrimSpace(string(window.Calendar.Unit))))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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
|
|
}
|