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,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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user