DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,126 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/registry"
|
||||
)
|
||||
|
||||
type NodeProbe func(context.Context, registry.Node) error
|
||||
|
||||
type ProbeOptions struct {
|
||||
Service string
|
||||
Timeout time.Duration
|
||||
Probe NodeProbe
|
||||
OnChange func(ProbeResult)
|
||||
}
|
||||
|
||||
type ProbeResult struct {
|
||||
Service string `json:"service"`
|
||||
NodeID string `json:"node_id"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
|
||||
func (r *Resolver) MustAlive(ctx context.Context, service, nodeID string) (bool, error) {
|
||||
node, ok, err := r.lookupNodeDirect(ctx, service, nodeID)
|
||||
if err != nil || !ok {
|
||||
return false, err
|
||||
}
|
||||
if r.rejectsMaintaining(node) {
|
||||
return false, nil
|
||||
}
|
||||
return nodeAccepting(node), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ProbeNodes(ctx context.Context, options ProbeOptions) ([]ProbeResult, error) {
|
||||
if r == nil || r.reg == nil {
|
||||
return nil, ErrNoRegistry
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
service := strings.TrimSpace(options.Service)
|
||||
if service == "" {
|
||||
return nil, ErrNoNodes
|
||||
}
|
||||
if options.Probe == nil {
|
||||
return nil, ErrNoProbe
|
||||
}
|
||||
nodes, err := r.reg.List(ctx, service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := r.now().UTC()
|
||||
results := make([]ProbeResult, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
if strings.TrimSpace(node.NodeID) == "" {
|
||||
continue
|
||||
}
|
||||
result := ProbeResult{
|
||||
Service: service,
|
||||
NodeID: node.NodeID,
|
||||
CheckedAt: now,
|
||||
}
|
||||
probeCtx := ctx
|
||||
cancel := func() {}
|
||||
if options.Timeout > 0 {
|
||||
probeCtx, cancel = context.WithTimeout(ctx, options.Timeout)
|
||||
}
|
||||
err := options.Probe(probeCtx, node)
|
||||
cancel()
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
r.MarkFailure(node.NodeID)
|
||||
} else {
|
||||
result.Healthy = true
|
||||
r.MarkSuccess(node.NodeID)
|
||||
}
|
||||
r.Invalidate(service)
|
||||
if options.OnChange != nil {
|
||||
options.OnChange(result)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupNodeDirect(ctx context.Context, service, nodeID string) (registry.Node, bool, error) {
|
||||
if r == nil || r.reg == nil {
|
||||
return registry.Node{}, false, ErrNoRegistry
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
service = strings.TrimSpace(service)
|
||||
nodeID = strings.TrimSpace(nodeID)
|
||||
if service == "" {
|
||||
return registry.Node{}, false, ErrNoNodes
|
||||
}
|
||||
if nodeID == "" {
|
||||
return registry.Node{}, false, nil
|
||||
}
|
||||
nodes, err := r.reg.List(ctx, service)
|
||||
if err != nil {
|
||||
return registry.Node{}, false, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if strings.TrimSpace(node.NodeID) == nodeID {
|
||||
return node, true, nil
|
||||
}
|
||||
}
|
||||
return registry.Node{}, false, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) rejectsMaintaining(node registry.Node) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
reject := r.rejectMaintaining
|
||||
r.mu.Unlock()
|
||||
return reject && nodeMaintaining(node.Meta)
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoRegistry = errors.New("discovery registry is required")
|
||||
ErrNoNodes = errors.New("discovery service has no nodes")
|
||||
ErrNoRoute = errors.New("discovery route has no nodes")
|
||||
ErrNoProbe = errors.New("discovery health probe is required")
|
||||
ErrRouteRequiresKey = errors.New("discovery route requires routing key")
|
||||
)
|
||||
|
||||
type Strategy string
|
||||
|
||||
const (
|
||||
StrategyHash Strategy = "hash"
|
||||
StrategyRandom Strategy = "random"
|
||||
StrategyRoundRobin Strategy = "round_robin"
|
||||
StrategyWeightedRoundRobin Strategy = "weighted_round_robin"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
reg registry.Registry
|
||||
cacheTTL time.Duration
|
||||
failureTTL time.Duration
|
||||
failureThreshold int
|
||||
rejectMaintaining bool
|
||||
strategy Strategy
|
||||
clock func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
cache map[string]cacheEntry
|
||||
failures map[string]failureState
|
||||
cursors map[string]uint64
|
||||
rng *rand.Rand
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
nodes []registry.Node
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type failureState struct {
|
||||
count int
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
CacheTTLSeconds int64 `json:"cache_ttl_seconds"`
|
||||
FailureTTLSeconds int64 `json:"failure_ttl_seconds"`
|
||||
FailureThreshold int `json:"failure_threshold"`
|
||||
RejectMaintaining bool `json:"reject_maintaining"`
|
||||
Strategy Strategy `json:"strategy"`
|
||||
CachedServices []CachedService `json:"cached_services"`
|
||||
Failures []FailureSnapshot `json:"failures"`
|
||||
}
|
||||
|
||||
type CachedService struct {
|
||||
Service string `json:"service"`
|
||||
NodeIDs []string `json:"node_ids"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type FailureSnapshot struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Count int `json:"count"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Suppressed bool `json:"suppressed"`
|
||||
}
|
||||
|
||||
func NewResolver(reg registry.Registry, cacheTTL, failureTTL time.Duration, rejectMaintaining bool) *Resolver {
|
||||
if cacheTTL <= 0 {
|
||||
cacheTTL = time.Second
|
||||
}
|
||||
if failureTTL <= 0 {
|
||||
failureTTL = 5 * time.Second
|
||||
}
|
||||
return &Resolver{
|
||||
reg: reg,
|
||||
cacheTTL: cacheTTL,
|
||||
failureTTL: failureTTL,
|
||||
failureThreshold: 1,
|
||||
rejectMaintaining: rejectMaintaining,
|
||||
strategy: StrategyHash,
|
||||
clock: time.Now,
|
||||
cache: make(map[string]cacheEntry),
|
||||
failures: make(map[string]failureState),
|
||||
cursors: make(map[string]uint64),
|
||||
rng: rand.New(rand.NewSource(time.Now().UnixNano())), // #nosec G404 -- 非安全用途:服务发现随机负载均衡,不用于凭证或加密。
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) Resolve(ctx context.Context, service, key string) (registry.Node, bool, error) {
|
||||
if r == nil || r.reg == nil {
|
||||
return registry.Node{}, false, ErrNoRegistry
|
||||
}
|
||||
service = strings.TrimSpace(service)
|
||||
if service == "" {
|
||||
return registry.Node{}, false, ErrNoNodes
|
||||
}
|
||||
nodes, err := r.nodes(ctx, service)
|
||||
if err != nil {
|
||||
return registry.Node{}, false, err
|
||||
}
|
||||
candidates := r.candidates(nodes)
|
||||
if len(candidates) == 0 {
|
||||
return registry.Node{}, false, nil
|
||||
}
|
||||
return r.selectNode(service, candidates, key), true, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveWithTags(ctx context.Context, service, key string, tags []string, matchAny bool) (registry.Node, bool, error) {
|
||||
if r == nil || r.reg == nil {
|
||||
return registry.Node{}, false, ErrNoRegistry
|
||||
}
|
||||
service = strings.TrimSpace(service)
|
||||
if service == "" {
|
||||
return registry.Node{}, false, ErrNoNodes
|
||||
}
|
||||
nodes, err := r.nodes(ctx, service)
|
||||
if err != nil {
|
||||
return registry.Node{}, false, err
|
||||
}
|
||||
candidates := filterNodesByTags(r.candidates(nodes), tags, matchAny)
|
||||
if len(candidates) == 0 {
|
||||
return registry.Node{}, false, nil
|
||||
}
|
||||
return r.selectNode(service+":"+tagBucket(tags, matchAny), candidates, key), true, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveRoute(ctx context.Context, service, route, key string) (registry.Node, bool, error) {
|
||||
if r == nil || r.reg == nil {
|
||||
return registry.Node{}, false, ErrNoRegistry
|
||||
}
|
||||
service = strings.TrimSpace(service)
|
||||
route = strings.TrimSpace(route)
|
||||
if service == "" {
|
||||
return registry.Node{}, false, ErrNoNodes
|
||||
}
|
||||
if route == "" {
|
||||
return r.Resolve(ctx, service, key)
|
||||
}
|
||||
nodes, err := r.nodes(ctx, service)
|
||||
if err != nil {
|
||||
return registry.Node{}, false, err
|
||||
}
|
||||
candidates, err := r.routeCandidates(nodes, route, key)
|
||||
if err != nil {
|
||||
return registry.Node{}, false, err
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
if nodesDeclareRoutes(nodes) {
|
||||
return registry.Node{}, false, ErrNoRoute
|
||||
}
|
||||
return registry.Node{}, false, nil
|
||||
}
|
||||
return r.selectNode(service+":"+route, candidates, key), true, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveRouteWithTags(ctx context.Context, service, route, key string, tags []string, matchAny bool) (registry.Node, bool, error) {
|
||||
if r == nil || r.reg == nil {
|
||||
return registry.Node{}, false, ErrNoRegistry
|
||||
}
|
||||
service = strings.TrimSpace(service)
|
||||
route = strings.TrimSpace(route)
|
||||
if service == "" {
|
||||
return registry.Node{}, false, ErrNoNodes
|
||||
}
|
||||
if route == "" {
|
||||
return r.ResolveWithTags(ctx, service, key, tags, matchAny)
|
||||
}
|
||||
nodes, err := r.nodes(ctx, service)
|
||||
if err != nil {
|
||||
return registry.Node{}, false, err
|
||||
}
|
||||
candidates, err := r.routeCandidates(nodes, route, key)
|
||||
if err != nil {
|
||||
return registry.Node{}, false, err
|
||||
}
|
||||
candidates = filterNodesByTags(candidates, tags, matchAny)
|
||||
if len(candidates) == 0 {
|
||||
if nodesDeclareRoutes(nodes) {
|
||||
return registry.Node{}, false, ErrNoRoute
|
||||
}
|
||||
return registry.Node{}, false, nil
|
||||
}
|
||||
return r.selectNode(service+":"+route+":"+tagBucket(tags, matchAny), candidates, key), true, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) MarkFailure(nodeID string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
nodeID = strings.TrimSpace(nodeID)
|
||||
if nodeID == "" {
|
||||
return
|
||||
}
|
||||
now := r.now()
|
||||
r.mu.Lock()
|
||||
r.pruneFailuresLocked(now)
|
||||
state := r.failures[nodeID]
|
||||
if state.expiresAt.IsZero() || !state.expiresAt.After(now) {
|
||||
state.count = 0
|
||||
}
|
||||
state.count++
|
||||
state.expiresAt = now.Add(r.failureTTL)
|
||||
r.failures[nodeID] = state
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Resolver) MarkSuccess(nodeID string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
nodeID = strings.TrimSpace(nodeID)
|
||||
if nodeID == "" {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
delete(r.failures, nodeID)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Resolver) Invalidate(service string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
service = strings.TrimSpace(service)
|
||||
r.mu.Lock()
|
||||
if service == "" {
|
||||
r.cache = make(map[string]cacheEntry)
|
||||
} else {
|
||||
delete(r.cache, service)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Resolver) SetFailureThreshold(threshold int) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if threshold <= 0 {
|
||||
threshold = 1
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.failureThreshold = threshold
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Resolver) Configure(cacheTTL, failureTTL time.Duration, failureThreshold int, rejectMaintaining bool, strategy Strategy) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if cacheTTL <= 0 {
|
||||
cacheTTL = time.Second
|
||||
}
|
||||
if failureTTL <= 0 {
|
||||
failureTTL = 5 * time.Second
|
||||
}
|
||||
if failureThreshold <= 0 {
|
||||
failureThreshold = 1
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.cacheTTL = cacheTTL
|
||||
r.failureTTL = failureTTL
|
||||
r.failureThreshold = failureThreshold
|
||||
r.rejectMaintaining = rejectMaintaining
|
||||
r.strategy = normalizeStrategy(strategy)
|
||||
r.cache = make(map[string]cacheEntry)
|
||||
r.cursors = make(map[string]uint64)
|
||||
r.pruneFailuresLocked(r.now())
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Resolver) SetStrategy(strategy Strategy) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.strategy = normalizeStrategy(strategy)
|
||||
r.cursors = make(map[string]uint64)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Resolver) Snapshot() Snapshot {
|
||||
if r == nil {
|
||||
return Snapshot{}
|
||||
}
|
||||
now := r.now()
|
||||
r.mu.Lock()
|
||||
r.pruneFailuresLocked(now)
|
||||
cache := make([]CachedService, 0, len(r.cache))
|
||||
for service, entry := range r.cache {
|
||||
nodeIDs := make([]string, 0, len(entry.nodes))
|
||||
for _, node := range entry.nodes {
|
||||
nodeID := strings.TrimSpace(node.NodeID)
|
||||
if nodeID != "" {
|
||||
nodeIDs = append(nodeIDs, nodeID)
|
||||
}
|
||||
}
|
||||
sort.Strings(nodeIDs)
|
||||
cache = append(cache, CachedService{
|
||||
Service: service,
|
||||
NodeIDs: nodeIDs,
|
||||
ExpiresAt: entry.expiresAt,
|
||||
})
|
||||
}
|
||||
failures := make([]FailureSnapshot, 0, len(r.failures))
|
||||
for nodeID, state := range r.failures {
|
||||
failures = append(failures, FailureSnapshot{
|
||||
NodeID: nodeID,
|
||||
Count: state.count,
|
||||
ExpiresAt: state.expiresAt,
|
||||
Suppressed: state.count >= r.failureThreshold && state.expiresAt.After(now),
|
||||
})
|
||||
}
|
||||
snapshot := Snapshot{
|
||||
CacheTTLSeconds: int64(r.cacheTTL / time.Second),
|
||||
FailureTTLSeconds: int64(r.failureTTL / time.Second),
|
||||
FailureThreshold: r.failureThreshold,
|
||||
RejectMaintaining: r.rejectMaintaining,
|
||||
Strategy: r.strategy,
|
||||
CachedServices: cache,
|
||||
Failures: failures,
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
sort.Slice(snapshot.CachedServices, func(i, j int) bool {
|
||||
return snapshot.CachedServices[i].Service < snapshot.CachedServices[j].Service
|
||||
})
|
||||
sort.Slice(snapshot.Failures, func(i, j int) bool {
|
||||
return snapshot.Failures[i].NodeID < snapshot.Failures[j].NodeID
|
||||
})
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (r *Resolver) nodes(ctx context.Context, service string) ([]registry.Node, error) {
|
||||
now := r.now()
|
||||
r.mu.Lock()
|
||||
if entry, ok := r.cache[service]; ok && entry.expiresAt.After(now) {
|
||||
nodes := cloneNodes(entry.nodes)
|
||||
r.mu.Unlock()
|
||||
return nodes, nil
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
nodes, err := r.reg.List(ctx, service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].NodeID < nodes[j].NodeID
|
||||
})
|
||||
|
||||
r.mu.Lock()
|
||||
r.cache[service] = cacheEntry{
|
||||
nodes: cloneNodes(nodes),
|
||||
expiresAt: now.Add(r.cacheTTL),
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) candidates(nodes []registry.Node) []registry.Node {
|
||||
now := r.now()
|
||||
r.mu.Lock()
|
||||
failures := make(map[string]struct{}, len(r.failures))
|
||||
rejectMaintaining := r.rejectMaintaining
|
||||
r.pruneFailuresLocked(now)
|
||||
for nodeID, state := range r.failures {
|
||||
if state.count >= r.failureThreshold && state.expiresAt.After(now) {
|
||||
failures[nodeID] = struct{}{}
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
out := make([]registry.Node, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
nodeID := strings.TrimSpace(node.NodeID)
|
||||
if nodeID == "" {
|
||||
continue
|
||||
}
|
||||
if _, failed := failures[nodeID]; failed {
|
||||
continue
|
||||
}
|
||||
if rejectMaintaining && nodeMaintaining(node.Meta) {
|
||||
continue
|
||||
}
|
||||
if !nodeAccepting(node) {
|
||||
continue
|
||||
}
|
||||
out = append(out, node)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Resolver) routeCandidates(nodes []registry.Node, routeID, key string) ([]registry.Node, error) {
|
||||
candidates := r.candidates(nodes)
|
||||
if len(candidates) == 0 || !nodesDeclareRoutes(candidates) {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
out := make([]registry.Node, 0, len(candidates))
|
||||
for _, node := range candidates {
|
||||
route, ok := matchRoute(node.Routes, routeID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(key) == "" && (route.Stateful || route.Authorized) {
|
||||
return nil, ErrRouteRequiresKey
|
||||
}
|
||||
out = append(out, node)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) selectNode(bucket string, candidates []registry.Node, key string) registry.Node {
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0]
|
||||
}
|
||||
r.mu.Lock()
|
||||
strategy := r.strategy
|
||||
r.mu.Unlock()
|
||||
switch strategy {
|
||||
case StrategyRandom:
|
||||
return candidates[r.randomIndex(len(candidates))]
|
||||
case StrategyRoundRobin:
|
||||
return candidates[r.nextCursor(bucket, len(candidates))]
|
||||
case StrategyWeightedRoundRobin:
|
||||
return r.selectWeightedRR(bucket, candidates)
|
||||
default:
|
||||
return selectWeightedByKey(candidates, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) nextCursor(bucket string, size int) int {
|
||||
if size <= 1 {
|
||||
return 0
|
||||
}
|
||||
r.mu.Lock()
|
||||
next := r.cursors[bucket]
|
||||
r.cursors[bucket] = next + 1
|
||||
r.mu.Unlock()
|
||||
return int(next % uint64(size)) // #nosec G115 -- 取模结果必定小于 size,size 已经是 int。
|
||||
}
|
||||
|
||||
func (r *Resolver) randomIndex(size int) int {
|
||||
if size <= 1 {
|
||||
return 0
|
||||
}
|
||||
r.mu.Lock()
|
||||
if r.rng == nil {
|
||||
r.rng = rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 -- 非安全用途:服务发现随机负载均衡,不用于凭证或加密。
|
||||
}
|
||||
index := r.rng.Intn(size)
|
||||
r.mu.Unlock()
|
||||
return index
|
||||
}
|
||||
|
||||
func (r *Resolver) selectWeightedRR(bucket string, candidates []registry.Node) registry.Node {
|
||||
total := 0
|
||||
for _, candidate := range candidates {
|
||||
total += nodeWeight(candidate)
|
||||
}
|
||||
if total <= 0 {
|
||||
return candidates[r.nextCursor(bucket, len(candidates))]
|
||||
}
|
||||
index := r.nextCursor(bucket, total)
|
||||
for _, candidate := range candidates {
|
||||
weight := nodeWeight(candidate)
|
||||
if index < weight {
|
||||
return candidate
|
||||
}
|
||||
index -= weight
|
||||
}
|
||||
return candidates[len(candidates)-1]
|
||||
}
|
||||
|
||||
func (r *Resolver) pruneFailuresLocked(now time.Time) {
|
||||
for nodeID, state := range r.failures {
|
||||
if !state.expiresAt.After(now) {
|
||||
delete(r.failures, nodeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) now() time.Time {
|
||||
if r.clock == nil {
|
||||
return time.Now()
|
||||
}
|
||||
return r.clock()
|
||||
}
|
||||
|
||||
func nodeMaintaining(meta map[string]string) bool {
|
||||
for _, key := range []string{"maintaining", "is_maintaining", "maintenance"} {
|
||||
value := strings.TrimSpace(strings.ToLower(meta[key]))
|
||||
switch value {
|
||||
case "1", "true", "yes", "y", "on":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nodeAccepting(node registry.Node) bool {
|
||||
switch strings.TrimSpace(strings.ToLower(nodeState(node))) {
|
||||
case "stopping", "stopped", "shut", "hang":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func nodeState(node registry.Node) string {
|
||||
if strings.TrimSpace(node.State) != "" {
|
||||
return strings.TrimSpace(node.State)
|
||||
}
|
||||
return strings.TrimSpace(node.Meta["state"])
|
||||
}
|
||||
|
||||
func nodeWeight(node registry.Node) int {
|
||||
if node.Weight > 0 {
|
||||
return node.Weight
|
||||
}
|
||||
weight, _ := strconv.Atoi(strings.TrimSpace(node.Meta["weight"]))
|
||||
if weight > 0 {
|
||||
return weight
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func nodesDeclareRoutes(nodes []registry.Node) bool {
|
||||
for _, node := range nodes {
|
||||
if len(node.Routes) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchRoute(routes []registry.Route, routeID string) (registry.Route, bool) {
|
||||
for _, route := range routes {
|
||||
if strings.TrimSpace(route.ID) == routeID {
|
||||
return route, true
|
||||
}
|
||||
}
|
||||
return registry.Route{}, false
|
||||
}
|
||||
|
||||
func filterNodesByTags(nodes []registry.Node, tags []string, matchAny bool) []registry.Node {
|
||||
tags = normalizeTags(tags)
|
||||
if len(tags) == 0 {
|
||||
return nodes
|
||||
}
|
||||
out := make([]registry.Node, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
if nodeMatchesTags(node, tags, matchAny) {
|
||||
out = append(out, node)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func nodeMatchesTags(node registry.Node, tags []string, matchAny bool) bool {
|
||||
nodeTags := normalizeTags(node.Tags)
|
||||
if len(nodeTags) == 0 {
|
||||
return false
|
||||
}
|
||||
have := make(map[string]struct{}, len(nodeTags))
|
||||
for _, tag := range nodeTags {
|
||||
have[tag] = struct{}{}
|
||||
}
|
||||
if matchAny {
|
||||
for _, tag := range tags {
|
||||
if _, ok := have[tag]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, ok := have[tag]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func tagBucket(tags []string, matchAny bool) string {
|
||||
tags = normalizeTags(tags)
|
||||
if len(tags) == 0 {
|
||||
return "all"
|
||||
}
|
||||
mode := "all"
|
||||
if matchAny {
|
||||
mode = "any"
|
||||
}
|
||||
encoded := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
encoded = append(encoded, encodeTagBucketPart(tag))
|
||||
}
|
||||
return mode + ":" + strings.Join(encoded, ",")
|
||||
}
|
||||
|
||||
func encodeTagBucketPart(tag string) string {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag == "" {
|
||||
return "_"
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(tag))
|
||||
}
|
||||
|
||||
func normalizeTags(tags []string) []string {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(tags))
|
||||
out := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
tag = strings.TrimSpace(strings.ToLower(tag))
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[tag]; ok {
|
||||
continue
|
||||
}
|
||||
seen[tag] = struct{}{}
|
||||
out = append(out, tag)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func selectWeightedByKey(candidates []registry.Node, key string) registry.Node {
|
||||
total := 0
|
||||
for _, candidate := range candidates {
|
||||
total += nodeWeight(candidate)
|
||||
}
|
||||
if total <= 0 {
|
||||
return candidates[hashIndex(key, len(candidates))]
|
||||
}
|
||||
index := hashIndex(key, total)
|
||||
for _, candidate := range candidates {
|
||||
weight := nodeWeight(candidate)
|
||||
if index < weight {
|
||||
return candidate
|
||||
}
|
||||
index -= weight
|
||||
}
|
||||
return candidates[len(candidates)-1]
|
||||
}
|
||||
|
||||
func hashIndex(key string, size int) int {
|
||||
if size <= 1 {
|
||||
return 0
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return 0
|
||||
}
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(key))
|
||||
return int(uint64(h.Sum32()) % uint64(size)) // #nosec G115 -- 取模结果必定小于 size,size 已经是 int。
|
||||
}
|
||||
|
||||
func normalizeStrategy(strategy Strategy) Strategy {
|
||||
value := Strategy(strings.ToLower(strings.TrimSpace(string(strategy))))
|
||||
switch value {
|
||||
case StrategyRandom, StrategyRoundRobin, StrategyWeightedRoundRobin:
|
||||
return value
|
||||
default:
|
||||
return StrategyHash
|
||||
}
|
||||
}
|
||||
|
||||
func cloneNodes(nodes []registry.Node) []registry.Node {
|
||||
out := append([]registry.Node(nil), nodes...)
|
||||
for idx := range out {
|
||||
out[idx].Meta = cloneMeta(out[idx].Meta)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user