DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+449
View File
@@ -0,0 +1,449 @@
package registry
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"path"
"sort"
"strings"
"sync"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
type ETCD struct {
logger *slog.Logger
client *clientv3.Client
prefix string
leaseTTL int64
mu sync.Mutex
closed bool
leases map[string]etcdLease
}
type etcdLease struct {
key string
lease clientv3.LeaseID
cancel context.CancelFunc
}
const etcdCleanupTimeout = 5 * time.Second
func NewETCD(endpoints []string, namespace string, leaseTTL int64, logger *slog.Logger) (*ETCD, error) {
endpoints = normalizeEndpoints(endpoints)
if len(endpoints) == 0 {
return nil, fmt.Errorf("etcd registry requires at least one endpoint")
}
if leaseTTL <= 0 {
leaseTTL = 15
}
client, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, err
}
return &ETCD{
logger: logger,
client: client,
prefix: normalizeNamespace(namespace),
leaseTTL: leaseTTL,
leases: make(map[string]etcdLease),
}, nil
}
func (r *ETCD) Register(ctx context.Context, node Node) error {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
if r == nil {
return ErrClosed
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return ErrClosed
}
client := r.client
r.mu.Unlock()
if client == nil {
return ErrClosed
}
node.Service = strings.TrimSpace(node.Service)
node.NodeID = strings.TrimSpace(node.NodeID)
node.Tags = normalizeTags(node.Tags)
if node.Service == "" || node.NodeID == "" {
return fmt.Errorf("registry node service and node_id are required")
}
node.RegisteredAt = time.Now().UTC()
data, err := json.Marshal(node)
if err != nil {
return err
}
leaseResp, err := client.Grant(ctx, r.leaseTTL)
if err != nil {
return err
}
key := r.nodeKey(node.Service, node.NodeID)
if _, err := client.Put(ctx, key, string(data), clientv3.WithLease(leaseResp.ID)); err != nil {
cleanupCtx, cleanupCancel := etcdCleanupContext(ctx)
_, _ = client.Revoke(cleanupCtx, leaseResp.ID)
cleanupCancel()
return err
}
keepAliveCtx, cancel := context.WithCancel(context.Background())
ch, err := client.KeepAlive(keepAliveCtx, leaseResp.ID)
if err != nil {
cancel()
cleanupCtx, cleanupCancel := etcdCleanupContext(ctx)
_, _ = client.Delete(cleanupCtx, key)
_, _ = client.Revoke(cleanupCtx, leaseResp.ID)
cleanupCancel()
return err
}
id := node.Service + "\x00" + node.NodeID
var oldLease etcdLease
var replaced bool
r.mu.Lock()
if existing, ok := r.leases[id]; ok {
existing.cancel()
oldLease = existing
replaced = true
}
if r.leases == nil {
r.leases = make(map[string]etcdLease)
}
r.leases[id] = etcdLease{
key: key,
lease: leaseResp.ID,
cancel: cancel,
}
r.mu.Unlock()
if replaced {
cleanupCtx, cleanupCancel := etcdCleanupContext(ctx)
_, _ = client.Revoke(cleanupCtx, oldLease.lease)
cleanupCancel()
}
go r.consumeKeepAlive(id, key, leaseResp.ID, ch)
return nil
}
func (r *ETCD) Check(ctx context.Context) error {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
if r == nil {
return ErrClosed
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return ErrClosed
}
client := r.client
prefix := r.prefix
r.mu.Unlock()
if client == nil {
return ErrClosed
}
_, err := client.Get(ctx, path.Join("/", prefix), clientv3.WithPrefix(), clientv3.WithLimit(1))
return err
}
func (r *ETCD) Deregister(ctx context.Context, service, nodeID string) error {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
service = strings.TrimSpace(service)
nodeID = strings.TrimSpace(nodeID)
if service == "" || nodeID == "" {
return nil
}
if r == nil {
return ErrClosed
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return ErrClosed
}
id := service + "\x00" + nodeID
lease, ok := r.leases[id]
client := r.client
r.mu.Unlock()
if client == nil {
return ErrClosed
}
key := r.nodeKey(service, nodeID)
if _, err := client.Delete(ctx, key); err != nil {
return err
}
if ok && r.removeLeaseIfCurrent(id, lease) {
lease.cancel()
cleanupCtx, cleanupCancel := etcdCleanupContext(ctx)
_, _ = client.Revoke(cleanupCtx, lease.lease)
cleanupCancel()
}
return nil
}
func (r *ETCD) List(ctx context.Context, service string) ([]Node, error) {
nodes, _, err := r.listWithRevision(ctx, service)
return nodes, err
}
func (r *ETCD) listWithRevision(ctx context.Context, service string) ([]Node, int64, error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return nil, 0, err
}
service = strings.TrimSpace(service)
if service == "" {
return nil, 0, nil
}
if r == nil {
return nil, 0, ErrClosed
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return nil, 0, ErrClosed
}
client := r.client
prefix := r.servicePrefix(service)
r.mu.Unlock()
if client == nil {
return nil, 0, ErrClosed
}
resp, err := client.Get(ctx, prefix, clientv3.WithPrefix())
if err != nil {
return nil, 0, err
}
nodes := make([]Node, 0, len(resp.Kvs))
for _, kv := range resp.Kvs {
var node Node
if err := json.Unmarshal(kv.Value, &node); err != nil {
if r.logger != nil {
r.logger.Warn("registry decode failed", "service", service, "key", string(kv.Key), "error", err)
}
continue
}
nodes = append(nodes, node)
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].NodeID < nodes[j].NodeID
})
revision := int64(0)
if resp.Header != nil {
revision = resp.Header.Revision
}
return nodes, revision, nil
}
func (r *ETCD) Watch(ctx context.Context, service string) (<-chan WatchEvent, error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
service = strings.TrimSpace(service)
if service == "" {
return nil, fmt.Errorf("registry watch service is required")
}
if r == nil {
return nil, ErrClosed
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return nil, ErrClosed
}
client := r.client
prefix := r.servicePrefix(service)
r.mu.Unlock()
if client == nil {
return nil, ErrClosed
}
initial, revision, err := r.listWithRevision(ctx, service)
if err != nil {
return nil, err
}
out := make(chan WatchEvent, 32)
go func() {
defer close(out)
for _, node := range initial {
select {
case out <- WatchEvent{Type: EventUpsert, Node: node}:
case <-ctx.Done():
return
}
}
watchOptions := []clientv3.OpOption{clientv3.WithPrefix()}
if revision > 0 {
watchOptions = append(watchOptions, clientv3.WithRev(revision+1))
}
watchCh := client.Watch(ctx, prefix, watchOptions...)
for resp := range watchCh {
if err := resp.Err(); err != nil {
if r.logger != nil {
r.logger.Warn("registry watch failed", "service", service, "error", err)
}
continue
}
for _, event := range resp.Events {
switch event.Type {
case clientv3.EventTypePut:
var node Node
if err := json.Unmarshal(event.Kv.Value, &node); err != nil {
if r.logger != nil {
r.logger.Warn("registry watch decode failed", "service", service, "key", string(event.Kv.Key), "error", err)
}
continue
}
select {
case out <- WatchEvent{Type: EventUpsert, Node: node}:
case <-ctx.Done():
return
}
case clientv3.EventTypeDelete:
select {
case out <- WatchEvent{Type: EventDelete, Node: r.nodeFromKey(service, string(event.Kv.Key))}:
case <-ctx.Done():
return
}
}
}
}
}()
return out, nil
}
func (r *ETCD) Close() error {
if r == nil {
return nil
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return nil
}
r.closed = true
leases := make([]etcdLease, 0, len(r.leases))
for _, lease := range r.leases {
leases = append(leases, lease)
}
r.leases = make(map[string]etcdLease)
client := r.client
r.client = nil
r.mu.Unlock()
if client == nil {
return nil
}
for _, lease := range leases {
lease.cancel()
cleanupCtx, cleanupCancel := etcdCleanupContext(nil)
_, _ = client.Revoke(cleanupCtx, lease.lease)
cleanupCancel()
}
return client.Close()
}
func (r *ETCD) nodeKey(service, nodeID string) string {
return path.Join("/", r.prefix, strings.TrimSpace(service), strings.TrimSpace(nodeID))
}
func (r *ETCD) servicePrefix(service string) string {
return path.Join("/", r.prefix, strings.TrimSpace(service)) + "/"
}
func (r *ETCD) nodeFromKey(service, key string) Node {
return Node{
Service: service,
NodeID: path.Base(key),
}
}
func (r *ETCD) consumeKeepAlive(id, key string, leaseID clientv3.LeaseID, ch <-chan *clientv3.LeaseKeepAliveResponse) {
for range ch {
}
r.mu.Lock()
lease, ok := r.leases[id]
if ok && lease.key == key && lease.lease == leaseID && !r.closed {
delete(r.leases, id)
}
closed := r.closed
r.mu.Unlock()
if !closed && r.logger != nil {
r.logger.Warn("registry keepalive stopped", "key", key)
}
}
func (r *ETCD) removeLeaseIfCurrent(id string, lease etcdLease) bool {
r.mu.Lock()
defer r.mu.Unlock()
current, ok := r.leases[id]
if !ok || current.key != lease.key || current.lease != lease.lease {
return false
}
delete(r.leases, id)
return true
}
func normalizeNamespace(namespace string) string {
namespace = strings.TrimSpace(namespace)
namespace = strings.Trim(namespace, "/")
if namespace == "" {
return "longheng"
}
return namespace
}
func etcdCleanupContext(parent context.Context) (context.Context, context.CancelFunc) {
base := context.Background()
if parent != nil {
base = context.WithoutCancel(parent)
}
return context.WithTimeout(base, etcdCleanupTimeout)
}
func normalizeEndpoints(endpoints []string) []string {
normalized := make([]string, 0, len(endpoints))
for _, endpoint := range endpoints {
endpoint = strings.TrimSpace(endpoint)
if endpoint != "" {
normalized = append(normalized, endpoint)
}
}
return normalized
}
+279
View File
@@ -0,0 +1,279 @@
package registry
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log/slog"
"sort"
"strings"
"sync"
"time"
"github.com/gomodule/redigo/redis"
)
var redisRegNodeScript = redis.NewScript(2, `
local ttl = tonumber(ARGV[3])
if ttl ~= nil and ttl > 0 then
redis.call("SETEX", KEYS[2], ttl, ARGV[2])
else
redis.call("SET", KEYS[2], ARGV[2])
end
return redis.call("SADD", KEYS[1], ARGV[1])
`)
// closeRedisConnErr 在主操作成功时把 Redis 连接关闭错误回传给调用方。
func closeRedisConnErr(conn redis.Conn, err *error) {
if conn == nil || err == nil {
return
}
if closeErr := conn.Close(); closeErr != nil && *err == nil {
*err = closeErr
}
}
var redisDropNodeScript = redis.NewScript(2, `
if redis.call("GET", KEYS[2]) == false then
return redis.call("SREM", KEYS[1], ARGV[1])
end
return 0
`)
type Redis struct {
logger *slog.Logger
pool *redis.Pool
prefix string
leaseTTL int
mu sync.Mutex
closed bool
}
func NewRedis(endpoints []string, namespace string, leaseTTL int64, password string, database int, logger *slog.Logger) (*Redis, error) {
endpoint := firstEndpoint(endpoints)
if endpoint == "" {
return nil, fmt.Errorf("redis registry requires at least one endpoint")
}
if leaseTTL <= 0 {
leaseTTL = 15
}
pool := &redis.Pool{
MaxIdle: 8,
MaxActive: 32,
IdleTimeout: time.Minute,
Wait: true,
Dial: func() (redis.Conn, error) {
dialOptions := []redis.DialOption{
redis.DialConnectTimeout(5 * time.Second),
redis.DialReadTimeout(5 * time.Second),
redis.DialWriteTimeout(5 * time.Second),
}
if password != "" {
dialOptions = append(dialOptions, redis.DialPassword(password))
}
if database > 0 {
dialOptions = append(dialOptions, redis.DialDatabase(database))
}
return redis.Dial(
"tcp",
endpoint,
dialOptions...,
)
},
TestOnBorrow: func(conn redis.Conn, lastUsed time.Time) error {
if time.Since(lastUsed) < time.Minute {
return nil
}
_, err := conn.Do("PING")
return err
},
}
return &Redis{
logger: logger,
pool: pool,
prefix: normalizeNamespace(namespace),
leaseTTL: int(leaseTTL),
}, nil
}
func (r *Redis) Register(ctx context.Context, node Node) (err error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
node.Service = strings.TrimSpace(node.Service)
node.NodeID = strings.TrimSpace(node.NodeID)
node.Tags = normalizeTags(node.Tags)
if node.Service == "" || node.NodeID == "" {
return fmt.Errorf("registry node service and node_id are required")
}
node.RegisteredAt = time.Now().UTC()
data, err := json.Marshal(node)
if err != nil {
return err
}
conn, err := r.getConn(ctx)
if err != nil {
return err
}
defer closeRedisConnErr(conn, &err)
_, err = redisRegNodeScript.Do(conn, r.serviceKey(node.Service), r.nodeKey(node.Service, node.NodeID), node.NodeID, data, r.leaseTTL)
return err
}
func (r *Redis) Check(ctx context.Context) (err error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
conn, err := r.getConn(ctx)
if err != nil {
return err
}
defer closeRedisConnErr(conn, &err)
_, err = redis.String(conn.Do("PING"))
return err
}
func (r *Redis) Deregister(ctx context.Context, service, nodeID string) (err error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
service = strings.TrimSpace(service)
nodeID = strings.TrimSpace(nodeID)
if service == "" || nodeID == "" {
return nil
}
conn, err := r.getConn(ctx)
if err != nil {
return err
}
defer closeRedisConnErr(conn, &err)
if _, err := conn.Do("DEL", r.nodeKey(service, nodeID)); err != nil {
return err
}
_, err = conn.Do("SREM", r.serviceKey(service), nodeID)
return err
}
func (r *Redis) List(ctx context.Context, service string) (nodes []Node, err error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
service = strings.TrimSpace(service)
if service == "" {
return nil, nil
}
conn, err := r.getConn(ctx)
if err != nil {
return nil, err
}
defer closeRedisConnErr(conn, &err)
nodeIDs, err := redis.Strings(conn.Do("SMEMBERS", r.serviceKey(service)))
if errors.Is(err, redis.ErrNil) {
return nil, nil
}
if err != nil {
return nil, err
}
nodes = make([]Node, 0, len(nodeIDs))
for _, nodeID := range nodeIDs {
nodeID = strings.TrimSpace(nodeID)
if nodeID == "" {
continue
}
data, err := redis.Bytes(conn.Do("GET", r.nodeKey(service, nodeID)))
if errors.Is(err, redis.ErrNil) {
_, _ = redisDropNodeScript.Do(conn, r.serviceKey(service), r.nodeKey(service, nodeID), nodeID)
continue
}
if err != nil {
return nil, err
}
var node Node
if err := json.Unmarshal(data, &node); err != nil {
if r.logger != nil {
r.logger.Warn("redis registry decode failed", "service", service, "node_id", nodeID, "error", err)
}
continue
}
nodes = append(nodes, node)
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].NodeID < nodes[j].NodeID
})
return nodes, nil
}
func (r *Redis) Close() error {
if r == nil {
return nil
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return nil
}
r.closed = true
pool := r.pool
r.pool = nil
r.mu.Unlock()
if pool == nil {
return nil
}
return pool.Close()
}
func (r *Redis) getConn(ctx context.Context) (redis.Conn, error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
if r == nil {
return nil, ErrClosed
}
r.mu.Lock()
if r.closed {
r.mu.Unlock()
return nil, ErrClosed
}
pool := r.pool
r.mu.Unlock()
if pool == nil {
return nil, ErrClosed
}
return pool.GetContext(ctx)
}
func (r *Redis) serviceKey(service string) string {
return r.prefix + ":registry:" + encodeRedisKeyPart(strings.TrimSpace(service)) + ":nodes"
}
func (r *Redis) nodeKey(service, nodeID string) string {
return r.prefix + ":registry:" + encodeRedisKeyPart(strings.TrimSpace(service)) + ":" + encodeRedisKeyPart(strings.TrimSpace(nodeID))
}
func encodeRedisKeyPart(value string) string {
if value == "" {
return "_"
}
return base64.RawURLEncoding.EncodeToString([]byte(value))
}
func firstEndpoint(endpoints []string) string {
for _, endpoint := range endpoints {
endpoint = strings.TrimSpace(endpoint)
if endpoint != "" {
return endpoint
}
}
return ""
}
@@ -0,0 +1,381 @@
package registry
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
)
var ErrClosed = errors.New("registry is closed")
type Node struct {
Service string `json:"service"`
NodeID string `json:"node_id"`
GID int64 `json:"gid,omitempty"`
VirtualGID int64 `json:"virtual_gid,omitempty"`
ShardID int64 `json:"shard_id,omitempty"`
PublicAddr string `json:"public_addr"`
PrivateAddr string `json:"private_addr"`
State string `json:"state,omitempty"`
Weight int `json:"weight,omitempty"`
Routes []Route `json:"routes,omitempty"`
Events []Event `json:"events,omitempty"`
Services []string `json:"services,omitempty"`
Tags []string `json:"tags,omitempty"`
GMVisible bool `json:"gm_visible,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
RegisteredAt time.Time `json:"registered_at"`
}
type Route 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 Event struct {
Name string `json:"name"`
}
type Registry interface {
Register(context.Context, Node) error
Deregister(context.Context, string, string) error
List(context.Context, string) ([]Node, error)
Close() error
}
type WatchEventType string
const (
EventUpsert WatchEventType = "upsert"
EventDelete WatchEventType = "delete"
)
type WatchEvent struct {
Type WatchEventType `json:"type"`
Node Node `json:"node"`
}
type Watcher interface {
Watch(context.Context, string) (<-chan WatchEvent, error)
}
type Memory struct {
mu sync.RWMutex
closed bool
closedCh chan struct{}
nodes map[string]map[string]Node
watchers map[string]map[chan WatchEvent]struct{}
}
func NewMemory() *Memory {
return &Memory{
closedCh: make(chan struct{}),
nodes: make(map[string]map[string]Node),
watchers: make(map[string]map[chan WatchEvent]struct{}),
}
}
func contextOrBackground(ctx context.Context) context.Context {
if ctx == nil {
return context.Background()
}
return ctx
}
func (r *Memory) Register(ctx context.Context, node Node) error {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
node.Service = strings.TrimSpace(node.Service)
node.NodeID = strings.TrimSpace(node.NodeID)
node.Tags = normalizeTags(node.Tags)
if node.Service == "" || node.NodeID == "" {
return fmt.Errorf("registry node service and node_id are required")
}
if r == nil {
return ErrClosed
}
r.mu.Lock()
r.ensureReadyLocked()
if r.closed {
r.mu.Unlock()
return ErrClosed
}
if _, ok := r.nodes[node.Service]; !ok {
r.nodes[node.Service] = make(map[string]Node)
}
node.RegisteredAt = time.Now().UTC()
node = cloneNode(node)
r.nodes[node.Service][node.NodeID] = node
r.publishLocked(ctx, node.Service, WatchEvent{Type: EventUpsert, Node: node})
r.mu.Unlock()
return nil
}
func (r *Memory) Check(ctx context.Context) error {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
if r == nil {
return ErrClosed
}
r.mu.RLock()
defer r.mu.RUnlock()
if r.closed {
return ErrClosed
}
return nil
}
func (r *Memory) Deregister(ctx context.Context, service, nodeID string) error {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
service = strings.TrimSpace(service)
nodeID = strings.TrimSpace(nodeID)
if service == "" || nodeID == "" {
return nil
}
if r == nil {
return ErrClosed
}
r.mu.Lock()
r.ensureReadyLocked()
if r.closed {
r.mu.Unlock()
return ErrClosed
}
node := Node{Service: service, NodeID: nodeID}
if serviceNodes, ok := r.nodes[service]; ok {
if existing, exists := serviceNodes[nodeID]; exists {
node = cloneNode(existing)
}
delete(serviceNodes, nodeID)
if len(serviceNodes) == 0 {
delete(r.nodes, service)
}
}
r.publishLocked(ctx, service, WatchEvent{Type: EventDelete, Node: node})
r.mu.Unlock()
return nil
}
func (r *Memory) List(ctx context.Context, service string) ([]Node, error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
service = strings.TrimSpace(service)
if service == "" {
return nil, nil
}
if r == nil {
return nil, ErrClosed
}
r.mu.RLock()
defer r.mu.RUnlock()
if r.closed {
return nil, ErrClosed
}
serviceNodes := r.nodes[service]
nodes := make([]Node, 0, len(serviceNodes))
for _, node := range serviceNodes {
nodes = append(nodes, cloneNode(node))
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].NodeID < nodes[j].NodeID
})
return nodes, nil
}
func ListGMVisible(ctx context.Context, reg Registry, service string) ([]Node, error) {
if reg == nil {
return nil, nil
}
nodes, err := reg.List(ctx, service)
if err != nil {
return nil, err
}
return FilterGMVisible(nodes), nil
}
func FilterGMVisible(nodes []Node) []Node {
if len(nodes) == 0 {
return nil
}
out := make([]Node, 0, len(nodes))
for _, node := range nodes {
if !node.GMVisible {
continue
}
out = append(out, cloneNode(node))
}
sort.Slice(out, func(i, j int) bool {
if out[i].Service != out[j].Service {
return out[i].Service < out[j].Service
}
return out[i].NodeID < out[j].NodeID
})
return out
}
func (r *Memory) Close() error {
if r == nil {
return nil
}
r.mu.Lock()
r.ensureReadyLocked()
if r.closed {
r.mu.Unlock()
return nil
}
r.closed = true
r.nodes = make(map[string]map[string]Node)
r.watchers = make(map[string]map[chan WatchEvent]struct{})
closedCh := r.closedCh
r.mu.Unlock()
close(closedCh)
return nil
}
func (r *Memory) Watch(ctx context.Context, service string) (<-chan WatchEvent, error) {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
service = strings.TrimSpace(service)
if service == "" {
return nil, fmt.Errorf("registry watch service is required")
}
if r == nil {
return nil, ErrClosed
}
r.mu.Lock()
r.ensureReadyLocked()
if r.closed {
r.mu.Unlock()
return nil, ErrClosed
}
initial := make([]Node, 0, len(r.nodes[service]))
for _, node := range r.nodes[service] {
initial = append(initial, cloneNode(node))
}
sort.Slice(initial, func(i, j int) bool {
return initial[i].NodeID < initial[j].NodeID
})
ch := make(chan WatchEvent, len(initial)+16)
for _, node := range initial {
ch <- WatchEvent{Type: EventUpsert, Node: node}
}
if _, ok := r.watchers[service]; !ok {
r.watchers[service] = make(map[chan WatchEvent]struct{})
}
r.watchers[service][ch] = struct{}{}
closedCh := r.closedCh
r.mu.Unlock()
go func() {
select {
case <-ctx.Done():
case <-closedCh:
}
r.mu.Lock()
if serviceWatchers, ok := r.watchers[service]; ok {
delete(serviceWatchers, ch)
if len(serviceWatchers) == 0 {
delete(r.watchers, service)
}
}
// publishLocked 与这里共用 r.mu,保证删除后关闭时没有发送者仍持有 ch。
close(ch)
r.mu.Unlock()
}()
return ch, nil
}
// ensureReadyLocked 补齐手工构造 Memory 时缺失的内部索引和关闭信号。
// 调用方必须已经持有 r.mu 写锁。
func (r *Memory) ensureReadyLocked() {
if r.closedCh == nil {
r.closedCh = make(chan struct{})
}
if r.nodes == nil {
r.nodes = make(map[string]map[string]Node)
}
if r.watchers == nil {
r.watchers = make(map[string]map[chan WatchEvent]struct{})
}
}
// publishLocked 只做非阻塞发送,调用方必须持有 r.mu 写锁。
// channel 的发送与关闭由同一把锁保护,不能再用 recover 掩盖生命周期竞争。
func (r *Memory) publishLocked(ctx context.Context, service string, event WatchEvent) {
if ctx != nil && ctx.Err() != nil {
return
}
for ch := range r.watchers[service] {
next := event
next.Node = cloneNode(event.Node)
select {
case ch <- next:
default:
}
}
}
func cloneNode(node Node) Node {
node.Meta = cloneMeta(node.Meta)
node.Routes = append([]Route(nil), node.Routes...)
node.Events = append([]Event(nil), node.Events...)
node.Services = append([]string(nil), node.Services...)
node.Tags = append([]string(nil), node.Tags...)
return node
}
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 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
}