服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
670 lines
19 KiB
Go
670 lines
19 KiB
Go
package topology
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/platform/registry"
|
|
)
|
|
|
|
const (
|
|
RoleGate = "gate"
|
|
RoleNode = "node"
|
|
RoleMesh = "mesh"
|
|
|
|
LinkGatewayBackend = "gateway_backend"
|
|
)
|
|
|
|
type Config struct {
|
|
Enabled bool
|
|
Services []string
|
|
GatewayService string
|
|
GatewayBackendService string
|
|
RejectMaintaining bool
|
|
RefreshInterval time.Duration
|
|
}
|
|
|
|
type Snapshot struct {
|
|
Enabled bool `json:"enabled"`
|
|
RefreshIntervalSeconds int64 `json:"refresh_interval_seconds"`
|
|
Services []ServiceSnapshot `json:"services"`
|
|
Gates []NodeSnapshot `json:"gates"`
|
|
Nodes []NodeSnapshot `json:"nodes"`
|
|
Mesh MeshSnapshot `json:"mesh"`
|
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type ServiceSnapshot struct {
|
|
Service string `json:"service"`
|
|
Role string `json:"role"`
|
|
TotalNodes int `json:"total_nodes"`
|
|
ReadyNodes int `json:"ready_nodes"`
|
|
MaintainingNodes int `json:"maintaining_nodes"`
|
|
Routes int `json:"routes"`
|
|
Events int `json:"events"`
|
|
MeshServices int `json:"mesh_services"`
|
|
}
|
|
|
|
type NodeSnapshot struct {
|
|
Service string `json:"service"`
|
|
NodeID string `json:"node_id"`
|
|
Role string `json:"role"`
|
|
ClusterID string `json:"cluster_id,omitempty"`
|
|
GID int64 `json:"gid,omitempty"`
|
|
ShardID int64 `json:"shard_id,omitempty"`
|
|
Environment string `json:"environment,omitempty"`
|
|
Zone string `json:"zone,omitempty"`
|
|
PublicAddr string `json:"public_addr,omitempty"`
|
|
PrivateAddr string `json:"private_addr,omitempty"`
|
|
AdminAddr string `json:"admin_addr,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
DataVersion string `json:"data_version,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
Weight int `json:"weight,omitempty"`
|
|
Maintaining bool `json:"maintaining"`
|
|
Accepting bool `json:"accepting"`
|
|
Routes []RouteSnapshot `json:"routes,omitempty"`
|
|
Events []EventSnapshot `json:"events,omitempty"`
|
|
Services []string `json:"services,omitempty"`
|
|
Observability *ObservabilitySnapshot `json:"observability,omitempty"`
|
|
Meta map[string]string `json:"meta,omitempty"`
|
|
RegisteredAt time.Time `json:"registered_at"`
|
|
}
|
|
|
|
type ObservabilitySnapshot struct {
|
|
MetricsEnabled bool `json:"metrics_enabled"`
|
|
MetricsPath string `json:"metrics_path,omitempty"`
|
|
MetricsURL string `json:"metrics_url,omitempty"`
|
|
MetricsSnapshotPath string `json:"metrics_snapshot_path,omitempty"`
|
|
MetricsSnapshotURL string `json:"metrics_snapshot_url,omitempty"`
|
|
MetricsRequireAdminToken bool `json:"metrics_require_admin_token,omitempty"`
|
|
PprofEnabled bool `json:"pprof_enabled"`
|
|
PprofPath string `json:"pprof_path,omitempty"`
|
|
PprofURL string `json:"pprof_url,omitempty"`
|
|
}
|
|
|
|
type RouteSnapshot struct {
|
|
ID string `json:"id"`
|
|
Group string `json:"group,omitempty"`
|
|
Internal bool `json:"internal,omitempty"`
|
|
Stateful bool `json:"stateful,omitempty"`
|
|
Authorized bool `json:"authorized,omitempty"`
|
|
Idempotent bool `json:"idempotent,omitempty"`
|
|
}
|
|
|
|
type EventSnapshot struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type MeshSnapshot struct {
|
|
Services []string `json:"services"`
|
|
Links []LinkSnapshot `json:"links"`
|
|
}
|
|
|
|
type LinkSnapshot struct {
|
|
Kind string `json:"kind"`
|
|
SourceService string `json:"source_service"`
|
|
TargetService string `json:"target_service"`
|
|
Available bool `json:"available"`
|
|
}
|
|
|
|
type Manager struct {
|
|
registry registry.Registry
|
|
logger *slog.Logger
|
|
|
|
mu sync.RWMutex
|
|
cfg Config
|
|
snapshot Snapshot
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
generation uint64
|
|
}
|
|
|
|
var detachedRootContext = context.Background()
|
|
|
|
func New(cfg Config, reg registry.Registry, logger *slog.Logger) *Manager {
|
|
cfg = normalizeConfig(cfg)
|
|
return &Manager{
|
|
registry: reg,
|
|
logger: logger,
|
|
cfg: cfg,
|
|
snapshot: Snapshot{
|
|
Enabled: cfg.Enabled,
|
|
RefreshIntervalSeconds: int64(cfg.RefreshInterval / time.Second),
|
|
Mesh: MeshSnapshot{Services: append([]string(nil), cfg.Services...)},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *Manager) Name() string {
|
|
return "topology-manager"
|
|
}
|
|
|
|
func (m *Manager) Start(ctx context.Context) error {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
m.mu.RLock()
|
|
generation := m.generation
|
|
m.mu.RUnlock()
|
|
return m.start(ctx, generation)
|
|
}
|
|
|
|
func (m *Manager) start(ctx context.Context, generation uint64) error {
|
|
m.mu.RLock()
|
|
enabled := m.cfg.Enabled
|
|
started := m.cancel != nil
|
|
interval := m.cfg.RefreshInterval
|
|
current := m.generation
|
|
m.mu.RUnlock()
|
|
if generation != current || started || !enabled {
|
|
return nil
|
|
}
|
|
if err := m.Refresh(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
m.mu.Lock()
|
|
if m.generation != generation || m.cancel != nil || !m.cfg.Enabled {
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
runCtx, cancel := context.WithCancel(detachedContext(ctx))
|
|
done := make(chan struct{})
|
|
m.cancel = cancel
|
|
m.done = done
|
|
m.mu.Unlock()
|
|
|
|
go m.loop(runCtx, done, interval)
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) AfterStart(ctx context.Context) error {
|
|
if m == nil || !m.Enabled() {
|
|
return nil
|
|
}
|
|
return m.Refresh(ctx)
|
|
}
|
|
|
|
func (m *Manager) Stop(ctx context.Context) error {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
m.mu.Lock()
|
|
cancel := m.cancel
|
|
done := m.done
|
|
m.generation++
|
|
m.cancel = nil
|
|
m.done = nil
|
|
m.mu.Unlock()
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
if done == nil {
|
|
return nil
|
|
}
|
|
select {
|
|
case <-done:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (m *Manager) Configure(cfg Config) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
cfg = normalizeConfig(cfg)
|
|
m.mu.Lock()
|
|
wasEnabled := m.cfg.Enabled
|
|
intervalChanged := m.cfg.RefreshInterval != cfg.RefreshInterval
|
|
cancel := m.cancel
|
|
done := m.done
|
|
m.generation++
|
|
generation := m.generation
|
|
m.cfg = cfg
|
|
m.snapshot.Enabled = cfg.Enabled
|
|
m.snapshot.RefreshIntervalSeconds = int64(cfg.RefreshInterval / time.Second)
|
|
if wasEnabled && (!cfg.Enabled || intervalChanged) {
|
|
m.cancel = nil
|
|
m.done = nil
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
if wasEnabled && (!cfg.Enabled || intervalChanged) && cancel != nil {
|
|
cancel()
|
|
}
|
|
if cfg.Enabled && (!wasEnabled || intervalChanged) {
|
|
if done != nil {
|
|
go m.startAfterDone(done, generation)
|
|
return
|
|
}
|
|
_ = m.start(detachedRootContext, generation)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) startAfterDone(done <-chan struct{}, generation uint64) {
|
|
<-done
|
|
_ = m.start(detachedRootContext, generation)
|
|
}
|
|
|
|
func detachedContext(ctx context.Context) context.Context {
|
|
if ctx == nil {
|
|
return detachedRootContext
|
|
}
|
|
return context.WithoutCancel(ctx)
|
|
}
|
|
|
|
func (m *Manager) Enabled() bool {
|
|
if m == nil {
|
|
return false
|
|
}
|
|
m.mu.RLock()
|
|
enabled := m.cfg.Enabled
|
|
m.mu.RUnlock()
|
|
return enabled
|
|
}
|
|
|
|
func (m *Manager) Refresh(ctx context.Context) error {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
m.mu.RLock()
|
|
cfg := m.cfg
|
|
reg := m.registry
|
|
m.mu.RUnlock()
|
|
if !cfg.Enabled {
|
|
m.storeSnapshot(buildSnapshot(cfg, nil, nil))
|
|
return nil
|
|
}
|
|
if reg == nil {
|
|
snapshot := buildSnapshot(cfg, nil, nil)
|
|
snapshot.Error = "registry is required"
|
|
m.storeSnapshot(snapshot)
|
|
return registry.ErrClosed
|
|
}
|
|
|
|
nodesByService := make(map[string][]registry.Node, len(cfg.Services))
|
|
var firstErr error
|
|
for _, service := range cfg.Services {
|
|
nodes, err := reg.List(ctx, service)
|
|
if err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
nodesByService[service] = nodes
|
|
}
|
|
snapshot := buildSnapshot(cfg, nodesByService, firstErr)
|
|
m.storeSnapshot(snapshot)
|
|
return firstErr
|
|
}
|
|
|
|
func (m *Manager) Snapshot() Snapshot {
|
|
if m == nil {
|
|
return Snapshot{}
|
|
}
|
|
m.mu.RLock()
|
|
snapshot := cloneSnapshot(m.snapshot)
|
|
m.mu.RUnlock()
|
|
return snapshot
|
|
}
|
|
|
|
func (m *Manager) loop(ctx context.Context, done chan struct{}, interval time.Duration) {
|
|
defer close(done)
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
if err := m.Refresh(ctx); err != nil && m.logger != nil {
|
|
m.logger.Warn("topology refresh failed", "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) storeSnapshot(snapshot Snapshot) {
|
|
m.mu.Lock()
|
|
m.snapshot = snapshot
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
func buildSnapshot(cfg Config, nodesByService map[string][]registry.Node, err error) Snapshot {
|
|
snapshot := Snapshot{
|
|
Enabled: cfg.Enabled,
|
|
RefreshIntervalSeconds: int64(cfg.RefreshInterval / time.Second),
|
|
UpdatedAt: time.Now().UTC(),
|
|
Mesh: MeshSnapshot{
|
|
Services: append([]string(nil), cfg.Services...),
|
|
},
|
|
}
|
|
if err != nil {
|
|
snapshot.Error = err.Error()
|
|
}
|
|
serviceReady := make(map[string]bool, len(cfg.Services))
|
|
for _, service := range cfg.Services {
|
|
role := roleForService(cfg, service)
|
|
nodes := nodesByService[service]
|
|
serviceSnapshot := ServiceSnapshot{Service: service, Role: role}
|
|
for _, node := range nodes {
|
|
nodeSnapshot := nodeFromRegistry(cfg, role, node)
|
|
serviceSnapshot.TotalNodes++
|
|
serviceSnapshot.Routes += len(nodeSnapshot.Routes)
|
|
serviceSnapshot.Events += len(nodeSnapshot.Events)
|
|
serviceSnapshot.MeshServices += len(nodeSnapshot.Services)
|
|
if nodeSnapshot.Maintaining {
|
|
serviceSnapshot.MaintainingNodes++
|
|
}
|
|
if nodeSnapshot.Accepting {
|
|
serviceSnapshot.ReadyNodes++
|
|
serviceReady[service] = true
|
|
}
|
|
if role == RoleGate {
|
|
snapshot.Gates = append(snapshot.Gates, nodeSnapshot)
|
|
} else {
|
|
snapshot.Nodes = append(snapshot.Nodes, nodeSnapshot)
|
|
}
|
|
}
|
|
snapshot.Services = append(snapshot.Services, serviceSnapshot)
|
|
}
|
|
if cfg.GatewayService != "" && cfg.GatewayBackendService != "" {
|
|
snapshot.Mesh.Links = append(snapshot.Mesh.Links, LinkSnapshot{
|
|
Kind: LinkGatewayBackend,
|
|
SourceService: cfg.GatewayService,
|
|
TargetService: cfg.GatewayBackendService,
|
|
Available: serviceReady[cfg.GatewayService] && serviceReady[cfg.GatewayBackendService],
|
|
})
|
|
}
|
|
sortSnapshot(&snapshot)
|
|
return snapshot
|
|
}
|
|
|
|
func nodeFromRegistry(cfg Config, role string, node registry.Node) NodeSnapshot {
|
|
meta := cloneMeta(node.Meta)
|
|
maintaining := parseBool(meta["maintaining"]) || parseBool(meta["is_maintaining"]) || parseBool(meta["maintenance"])
|
|
state := firstNonEmpty(node.State, meta["state"])
|
|
accepting := state != "stopping" && state != "stopped" && (!cfg.RejectMaintaining || !maintaining)
|
|
return NodeSnapshot{
|
|
Service: strings.TrimSpace(node.Service),
|
|
NodeID: strings.TrimSpace(node.NodeID),
|
|
Role: role,
|
|
ClusterID: strings.TrimSpace(meta["cluster_id"]),
|
|
GID: parseInt64(meta["gid"]),
|
|
ShardID: parseInt64(meta["shard_id"]),
|
|
Environment: firstNonEmpty(meta["environment"], meta["env"]),
|
|
Zone: strings.TrimSpace(meta["zone"]),
|
|
PublicAddr: firstNonEmpty(node.PublicAddr, meta["public_addr"]),
|
|
PrivateAddr: firstNonEmpty(node.PrivateAddr, meta["private_addr"]),
|
|
AdminAddr: strings.TrimSpace(meta["admin_addr"]),
|
|
Version: strings.TrimSpace(meta["version"]),
|
|
DataVersion: strings.TrimSpace(meta["data_version"]),
|
|
State: state,
|
|
Weight: nodeWeight(node),
|
|
Maintaining: maintaining,
|
|
Accepting: accepting,
|
|
Routes: routesFromRegistry(node.Routes),
|
|
Events: eventsFromRegistry(node.Events),
|
|
Services: cloneStrings(node.Services),
|
|
Observability: obsFromMeta(meta),
|
|
Meta: meta,
|
|
RegisteredAt: node.RegisteredAt,
|
|
}
|
|
}
|
|
|
|
func normalizeConfig(cfg Config) Config {
|
|
cfg.Services = normalizeServices(cfg.Services)
|
|
cfg.GatewayService = strings.TrimSpace(cfg.GatewayService)
|
|
cfg.GatewayBackendService = strings.TrimSpace(cfg.GatewayBackendService)
|
|
if cfg.RefreshInterval <= 0 {
|
|
cfg.RefreshInterval = 5 * time.Second
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func normalizeServices(services []string) []string {
|
|
seen := make(map[string]struct{}, len(services))
|
|
out := make([]string, 0, len(services))
|
|
for _, service := range services {
|
|
service = strings.TrimSpace(service)
|
|
if service == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[service]; ok {
|
|
continue
|
|
}
|
|
seen[service] = struct{}{}
|
|
out = append(out, service)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func roleForService(cfg Config, service string) string {
|
|
if strings.TrimSpace(service) == cfg.GatewayService {
|
|
return RoleGate
|
|
}
|
|
if cfg.GatewayBackendService != "" && strings.TrimSpace(service) != cfg.GatewayBackendService {
|
|
return RoleMesh
|
|
}
|
|
return RoleNode
|
|
}
|
|
|
|
func sortSnapshot(snapshot *Snapshot) {
|
|
sort.Slice(snapshot.Services, func(i, j int) bool {
|
|
return snapshot.Services[i].Service < snapshot.Services[j].Service
|
|
})
|
|
sort.Slice(snapshot.Gates, func(i, j int) bool {
|
|
return snapshot.Gates[i].NodeID < snapshot.Gates[j].NodeID
|
|
})
|
|
sort.Slice(snapshot.Nodes, func(i, j int) bool {
|
|
if snapshot.Nodes[i].Service != snapshot.Nodes[j].Service {
|
|
return snapshot.Nodes[i].Service < snapshot.Nodes[j].Service
|
|
}
|
|
return snapshot.Nodes[i].NodeID < snapshot.Nodes[j].NodeID
|
|
})
|
|
sort.Slice(snapshot.Mesh.Links, func(i, j int) bool {
|
|
if snapshot.Mesh.Links[i].SourceService != snapshot.Mesh.Links[j].SourceService {
|
|
return snapshot.Mesh.Links[i].SourceService < snapshot.Mesh.Links[j].SourceService
|
|
}
|
|
return snapshot.Mesh.Links[i].TargetService < snapshot.Mesh.Links[j].TargetService
|
|
})
|
|
}
|
|
|
|
func cloneSnapshot(snapshot Snapshot) Snapshot {
|
|
snapshot.Services = append([]ServiceSnapshot(nil), snapshot.Services...)
|
|
snapshot.Gates = append([]NodeSnapshot(nil), snapshot.Gates...)
|
|
for idx := range snapshot.Gates {
|
|
snapshot.Gates[idx].Meta = cloneMeta(snapshot.Gates[idx].Meta)
|
|
snapshot.Gates[idx].Routes = append([]RouteSnapshot(nil), snapshot.Gates[idx].Routes...)
|
|
snapshot.Gates[idx].Events = append([]EventSnapshot(nil), snapshot.Gates[idx].Events...)
|
|
snapshot.Gates[idx].Services = cloneStrings(snapshot.Gates[idx].Services)
|
|
snapshot.Gates[idx].Observability = cloneObservability(snapshot.Gates[idx].Observability)
|
|
}
|
|
snapshot.Nodes = append([]NodeSnapshot(nil), snapshot.Nodes...)
|
|
for idx := range snapshot.Nodes {
|
|
snapshot.Nodes[idx].Meta = cloneMeta(snapshot.Nodes[idx].Meta)
|
|
snapshot.Nodes[idx].Routes = append([]RouteSnapshot(nil), snapshot.Nodes[idx].Routes...)
|
|
snapshot.Nodes[idx].Events = append([]EventSnapshot(nil), snapshot.Nodes[idx].Events...)
|
|
snapshot.Nodes[idx].Services = cloneStrings(snapshot.Nodes[idx].Services)
|
|
snapshot.Nodes[idx].Observability = cloneObservability(snapshot.Nodes[idx].Observability)
|
|
}
|
|
snapshot.Mesh.Services = append([]string(nil), snapshot.Mesh.Services...)
|
|
snapshot.Mesh.Links = append([]LinkSnapshot(nil), snapshot.Mesh.Links...)
|
|
return snapshot
|
|
}
|
|
|
|
func cloneMeta(meta map[string]string) map[string]string {
|
|
if len(meta) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(meta))
|
|
for key, value := range meta {
|
|
out[key] = value
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneObservability(observability *ObservabilitySnapshot) *ObservabilitySnapshot {
|
|
if observability == nil {
|
|
return nil
|
|
}
|
|
out := *observability
|
|
return &out
|
|
}
|
|
|
|
func obsFromMeta(meta map[string]string) *ObservabilitySnapshot {
|
|
metricsEnabled := parseBool(meta["metrics_enabled"])
|
|
pprofEnabled := parseBool(meta["pprof_enabled"])
|
|
metricsPath := ""
|
|
if metricsEnabled {
|
|
metricsPath = firstNonEmpty(meta["metrics_path"], "/metrics")
|
|
}
|
|
metricsSnapPath := strings.TrimSpace(meta["metrics_snapshot_path"])
|
|
if metricsSnapPath == "" && meta["metrics_enabled"] != "" {
|
|
metricsSnapPath = "/debug/metrics"
|
|
}
|
|
pprofPath := ""
|
|
if pprofEnabled {
|
|
pprofPath = firstNonEmpty(meta["pprof_path"], "/debug/pprof/")
|
|
}
|
|
if !metricsEnabled && !pprofEnabled && metricsSnapPath == "" {
|
|
return nil
|
|
}
|
|
adminAddr := strings.TrimSpace(meta["admin_addr"])
|
|
return &ObservabilitySnapshot{
|
|
MetricsEnabled: metricsEnabled,
|
|
MetricsPath: metricsPath,
|
|
MetricsURL: endpointURL(adminAddr, metricsPath),
|
|
MetricsSnapshotPath: metricsSnapPath,
|
|
MetricsSnapshotURL: endpointURL(adminAddr, metricsSnapPath),
|
|
MetricsRequireAdminToken: parseBool(meta["metrics_require_admin_token"]),
|
|
PprofEnabled: pprofEnabled,
|
|
PprofPath: pprofPath,
|
|
PprofURL: endpointURL(adminAddr, pprofPath),
|
|
}
|
|
}
|
|
|
|
func endpointURL(addr, path string) string {
|
|
addr = strings.TrimSpace(addr)
|
|
path = strings.TrimSpace(path)
|
|
if addr == "" || path == "" {
|
|
return ""
|
|
}
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = "/" + path
|
|
}
|
|
if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") {
|
|
return strings.TrimRight(addr, "/") + path
|
|
}
|
|
if strings.HasPrefix(addr, ":") {
|
|
return "http://127.0.0.1" + addr + path
|
|
}
|
|
return "http://" + addr + path
|
|
}
|
|
|
|
func parseBool(value string) bool {
|
|
switch strings.TrimSpace(strings.ToLower(value)) {
|
|
case "1", "true", "yes", "y", "on":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func parseInt64(value string) int64 {
|
|
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
|
return parsed
|
|
}
|
|
|
|
func parseInt(value string) int {
|
|
parsed, _ := strconv.Atoi(strings.TrimSpace(value))
|
|
return parsed
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func nodeWeight(node registry.Node) int {
|
|
if node.Weight > 0 {
|
|
return node.Weight
|
|
}
|
|
weight := parseInt(node.Meta["weight"])
|
|
if weight > 0 {
|
|
return weight
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func routesFromRegistry(routes []registry.Route) []RouteSnapshot {
|
|
if len(routes) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]RouteSnapshot, 0, len(routes))
|
|
for _, route := range routes {
|
|
id := strings.TrimSpace(route.ID)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
out = append(out, RouteSnapshot{
|
|
ID: id,
|
|
Group: strings.TrimSpace(route.Group),
|
|
Internal: route.Internal,
|
|
Stateful: route.Stateful,
|
|
Authorized: route.Authorized,
|
|
Idempotent: route.Idempotent,
|
|
})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].ID < out[j].ID
|
|
})
|
|
return out
|
|
}
|
|
|
|
func eventsFromRegistry(events []registry.Event) []EventSnapshot {
|
|
if len(events) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]EventSnapshot, 0, len(events))
|
|
for _, event := range events {
|
|
name := strings.TrimSpace(event.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
out = append(out, EventSnapshot{Name: name})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].Name < out[j].Name
|
|
})
|
|
return out
|
|
}
|
|
|
|
func cloneStrings(values []string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := append([]string(nil), values...)
|
|
sort.Strings(out)
|
|
return out
|
|
}
|