Compare commits
No commits in common. "v1.0.10" and "master" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
.log.meta.json
|
|
||||||
44
AppClient.go
44
AppClient.go
@ -8,64 +8,58 @@ import (
|
|||||||
|
|
||||||
// AppClient 用于管理单个请求的重试和负载均衡状态
|
// AppClient 用于管理单个请求的重试和负载均衡状态
|
||||||
type AppClient struct {
|
type AppClient struct {
|
||||||
discoverer *Discoverer
|
excludes map[string]bool
|
||||||
excludes map[string]bool // 本次请求已排除的节点
|
tryTimes int
|
||||||
attempts int // 本次请求的重试次数
|
Logger *log.Logger
|
||||||
Logger *log.Logger // 用于日志记录的 Logger
|
App string
|
||||||
App string // 目标应用名称
|
Method string
|
||||||
Method string // 请求方法
|
Path string
|
||||||
Path string // 请求路径
|
Data *map[string]any
|
||||||
Data map[string]any // 请求数据
|
Headers *map[string]string
|
||||||
Headers map[string]string // 请求头
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// logError 记录 Discover 客户端错误
|
func (ac *AppClient) logError(error string, extra ...any) {
|
||||||
func (ac *AppClient) logError(msg string, extra ...any) {
|
|
||||||
if ac.Logger == nil {
|
if ac.Logger == nil {
|
||||||
ac.Logger = log.DefaultLogger
|
ac.Logger = log.DefaultLogger
|
||||||
}
|
}
|
||||||
ac.Logger.Error("Discover Client: "+msg, extra...)
|
ac.Logger.Error("Discover Client: "+error, extra...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next 获取下一个可用节点
|
|
||||||
func (ac *AppClient) Next(app string, request *http.Request) *NodeInfo {
|
func (ac *AppClient) Next(app string, request *http.Request) *NodeInfo {
|
||||||
|
|
||||||
return ac.NextWithNode(app, "", request)
|
return ac.NextWithNode(app, "", request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckApp 检查并尝试添加应用
|
|
||||||
func (ac *AppClient) CheckApp(app string) bool {
|
func (ac *AppClient) CheckApp(app string) bool {
|
||||||
nodes := ac.discoverer.GetAppNodes(app)
|
nodes := getAppNodes(app)
|
||||||
if nodes == nil {
|
if nodes == nil {
|
||||||
if !ac.discoverer.AddExternalApp(app, CallConfig{}) {
|
if !addApp(app, "", true) {
|
||||||
ac.logError("app not found", "app", app, "calls", ac.discoverer.config.Calls)
|
ac.logError("app not found", "app", app, "calls", Config.Calls)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextWithNode 获取下一个可用节点,支持指定节点
|
|
||||||
func (ac *AppClient) NextWithNode(app, withNode string, request *http.Request) *NodeInfo {
|
func (ac *AppClient) NextWithNode(app, withNode string, request *http.Request) *NodeInfo {
|
||||||
if ac.excludes == nil {
|
if ac.excludes == nil {
|
||||||
ac.excludes = make(map[string]bool)
|
ac.excludes = make(map[string]bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
allNodes := ac.discoverer.GetAppNodes(app)
|
allNodes := getAppNodes(app)
|
||||||
if len(allNodes) == 0 {
|
if len(allNodes) == 0 {
|
||||||
ac.logError("node not found", "app", app)
|
ac.logError("node not found", "app", app)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ac.attempts++
|
ac.tryTimes++
|
||||||
if withNode != "" {
|
if withNode != "" {
|
||||||
ac.excludes[withNode] = true
|
ac.excludes[withNode] = true
|
||||||
return allNodes[withNode]
|
return allNodes[withNode]
|
||||||
}
|
}
|
||||||
|
|
||||||
readyNodes := make([]*NodeInfo, 0, len(allNodes))
|
readyNodes := make([]*NodeInfo, 0)
|
||||||
for _, node := range allNodes {
|
for _, node := range allNodes {
|
||||||
if ac.excludes[node.Addr] || node.FailedTimes.Load() >= int32(ac.discoverer.config.CallRetryTimes) {
|
if ac.excludes[node.Addr] || node.FailedTimes >= Config.CallRetryTimes {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
readyNodes = append(readyNodes, node)
|
readyNodes = append(readyNodes, node)
|
||||||
@ -82,14 +76,14 @@ func (ac *AppClient) NextWithNode(app, withNode string, request *http.Request) *
|
|||||||
|
|
||||||
var node *NodeInfo
|
var node *NodeInfo
|
||||||
if len(readyNodes) > 0 {
|
if len(readyNodes) > 0 {
|
||||||
node = ac.discoverer.settedLoadBalancer.Next(ac, readyNodes, request)
|
node = settedLoadBalancer.Next(ac, readyNodes, request)
|
||||||
if node != nil {
|
if node != nil {
|
||||||
ac.excludes[node.Addr] = true
|
ac.excludes[node.Addr] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
ac.logError("no available node", "app", app, "attempts", ac.attempts)
|
ac.logError("no available node", "app", app, "tryTimes", ac.tryTimes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return node
|
||||||
|
|||||||
51
CHANGELOG.md
51
CHANGELOG.md
@ -1,48 +1,9 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## v1.0.10 (2026-05-09)
|
|
||||||
- **API Redesign (Elegant API)**:
|
|
||||||
- 引入包级泛型便捷调用:`Get[T]`, `Post[T]`, `Put[T]`, `Delete[T]`。
|
|
||||||
- 引入 `From(r *http.Request)` 包装器,优雅实现微服务 Header 自动透传。
|
|
||||||
- 统一 API 入口,区分服务端(`Start`/`Stop`)与客户端(`Get`/`Post`/`Do`)模式。
|
|
||||||
- 移除晦涩的 `CallT` 泛型辅助函数。
|
|
||||||
- 增加 `SetApp`, `SetRegistry`, `SetWeight` 便捷配置接口。
|
|
||||||
- **Infrastructure Alignment**:
|
|
||||||
- 耗时统计切换至 `go/timer` 高性能引擎。
|
|
||||||
- 服务令牌(Token)采用 `go/safe.SafeBuf` 内存安全保护。
|
|
||||||
- 依赖更新:`go/http` 升级至 `v1.0.10`。
|
|
||||||
- **Stability**:
|
|
||||||
- 优化 `Stop()` 逻辑:彻底重置实例状态,支持在单进程多次启动/停止(如单元测试场景)。
|
|
||||||
- 优化 `Init()` 逻辑:支持程序化配置与文件配置的智能合并。
|
|
||||||
- 修复 `AddExternalApp` 可能导致的订阅死锁问题。
|
|
||||||
|
|
||||||
## v1.0.9 (2026-05-05)
|
|
||||||
- **Stability & Testing**:
|
|
||||||
- 修复 `AddExternalApp` 在新客户端场景下可能遗漏同步拉取节点的问题。
|
|
||||||
- 优化测试用例性能:将 Mock Server 默认超时导致的 100s 阻塞通过强制 HTTP/1.1 配置解决。
|
|
||||||
- 增强测试健壮性:全面改用动态端口(Port 0)避开冲突,并利用 Redis URL 唯一 ID 隔离多实例间的 PubSub 干扰。
|
|
||||||
- 改进守护进程退出逻辑:使用 `select` 非阻塞模式确保 `Stop()` 后能立即响应并优雅关闭。
|
|
||||||
|
|
||||||
## v1.0.4 (2026-05-05)
|
|
||||||
- 稳定性增强:在 `addApp` 中引入“写时复制”(Copy-on-Write)机制,通过对配置 Map 进行深拷贝,彻底消除了高并发下配置读取与修改导致的 `concurrent map read and map write` 崩溃风险。
|
|
||||||
- 状态一致性优化:确保默认实例在动态添加应用后,能够同步更新包级别的全局 `Config` 变量,保证业务代码通过不同路径读取配置的一致性。
|
|
||||||
|
|
||||||
## v1.0.3 (2026-05-05)
|
|
||||||
- 架构深度优化:将 HTTP 客户端连接池(`appClientPools`)移入 `Discoverer` 实例,实现完全的资源隔离。
|
|
||||||
- 并发安全增强:引入读写锁保护 `Config` 结构,防止高并发下的配置读写冲突。
|
|
||||||
- 生命周期管理优化:使用 `atomic.Bool` 管理 `daemonRunning` 状态,确保线程安全。
|
|
||||||
- 资源回收机制:在 `Stop()` 方法中新增 HTTP 连接池清理逻辑(调用 `Destroy` 释放闲置连接),防止内存与句柄泄漏。
|
|
||||||
- 接口严谨性:将 `Discoverer` 内部字段(如 `config`)设为私有,通过 `GetConfig`/`SetConfig` 统一访问。
|
|
||||||
|
|
||||||
## v1.0.2
|
|
||||||
- 架构重构:支持多 Discoverer 实例,消灭包级全局状态。
|
|
||||||
- 兼容性:保留包级 API 转发至 `DefaultDiscoverer`。
|
|
||||||
|
|
||||||
## v1.0.1
|
|
||||||
- 优化代码规范:修复变量名冲突,改进命名语义。
|
|
||||||
- 性能优化:优化 `AppClient` 类型,减少寻址开销。
|
|
||||||
- 故障隔离:实现本地隔离机制,不再篡跨全局 Redis 状态。
|
|
||||||
- 压力缓解:心跳间隔优化至 5 秒。
|
|
||||||
|
|
||||||
## v1.0.0
|
## v1.0.0
|
||||||
- 初始版本:从 `ssgo/discover` 迁移并重构。
|
- 从 `ssgo/discover` 迁移至 `apigo.cc/go/discover`。
|
||||||
|
- 采用全新的 `apigo.cc/go` 基础设施(log, redis, http, cast, u)。
|
||||||
|
- 优化了注册中心同步机制,使用 `redis.Subscribe` 简化 PubSub 处理。
|
||||||
|
- 增强了负载均衡算法,引入更精确的得分计算。
|
||||||
|
- 统一了 Header 定义,对齐 `go/http` 标准。
|
||||||
|
- 移除所有 `panic`,通过 `error` 返回和日志记录确保系统稳定性。
|
||||||
|
|||||||
153
Caller.go
153
Caller.go
@ -5,26 +5,29 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
gohttp "apigo.cc/go/http"
|
gohttp "apigo.cc/go/http"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"apigo.cc/go/timer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Discoverer) getHttpClient(app string, timeout time.Duration, h2c bool) *gohttp.Client {
|
var appClientPools = make(map[string]*gohttp.Client)
|
||||||
d.appClientPoolsLock.RLock()
|
var appClientPoolsLock sync.RWMutex
|
||||||
c := d.appClientPools[app]
|
|
||||||
d.appClientPoolsLock.RUnlock()
|
func getHttpClient(app string, timeout time.Duration, h2c bool) *gohttp.Client {
|
||||||
|
appClientPoolsLock.RLock()
|
||||||
|
c := appClientPools[app]
|
||||||
|
appClientPoolsLock.RUnlock()
|
||||||
if c != nil {
|
if c != nil {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
d.appClientPoolsLock.Lock()
|
appClientPoolsLock.Lock()
|
||||||
defer d.appClientPoolsLock.Unlock()
|
defer appClientPoolsLock.Unlock()
|
||||||
c = d.appClientPools[app]
|
c = appClientPools[app]
|
||||||
if c != nil {
|
if c != nil {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@ -34,101 +37,52 @@ func (d *Discoverer) getHttpClient(app string, timeout time.Duration, h2c bool)
|
|||||||
} else {
|
} else {
|
||||||
c = gohttp.NewClient(timeout)
|
c = gohttp.NewClient(timeout)
|
||||||
}
|
}
|
||||||
d.appClientPools[app] = c
|
appClientPools[app] = c
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caller 用于发起服务间调用
|
|
||||||
type Caller struct {
|
type Caller struct {
|
||||||
discoverer *Discoverer
|
Request *http.Request
|
||||||
Request *http.Request // 原始请求,用于透传 Header
|
NoBody bool
|
||||||
NoBody bool // 是否不发送请求体
|
logger *log.Logger
|
||||||
logger *log.Logger // 用于日志记录的 Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) From(request *http.Request) *Caller {
|
func NewCaller(request *http.Request, logger *log.Logger) *Caller {
|
||||||
return &Caller{discoverer: d, Request: request, logger: d.logger}
|
return &Caller{Request: request, logger: logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) NewCaller(request *http.Request, logger *log.Logger) *Caller {
|
func (c *Caller) logError(error string, extra ...any) {
|
||||||
return &Caller{discoverer: d, Request: request, logger: logger}
|
|
||||||
}
|
|
||||||
|
|
||||||
// logError 记录 Discover 调用器错误
|
|
||||||
func (c *Caller) logError(msg string, extra ...any) {
|
|
||||||
if c.logger == nil {
|
if c.logger == nil {
|
||||||
c.logger = log.DefaultLogger
|
c.logger = log.DefaultLogger
|
||||||
}
|
}
|
||||||
c.logger.Error("Discover Caller: "+msg, extra...)
|
c.logger.Error("Discover Caller: "+error, extra...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 发起 GET 请求
|
|
||||||
func (c *Caller) Get(app, path string, headers ...string) *gohttp.Result {
|
func (c *Caller) Get(app, path string, headers ...string) *gohttp.Result {
|
||||||
return c.Do("GET", app, path, nil, headers...)
|
return c.Do("GET", app, path, nil, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post 发起 POST 请求
|
|
||||||
func (c *Caller) Post(app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Post(app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
return c.Do("POST", app, path, data, headers...)
|
return c.Do("POST", app, path, data, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put 发起 PUT 请求
|
|
||||||
func (c *Caller) Put(app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Put(app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
return c.Do("PUT", app, path, data, headers...)
|
return c.Do("PUT", app, path, data, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 发起 DELETE 请求
|
|
||||||
func (c *Caller) Delete(app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Delete(app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
return c.Do("DELETE", app, path, data, headers...)
|
return c.Do("DELETE", app, path, data, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Head 发起 HEAD 请求
|
|
||||||
func (c *Caller) Head(app, path string, headers ...string) *gohttp.Result {
|
func (c *Caller) Head(app, path string, headers ...string) *gohttp.Result {
|
||||||
return c.Do("HEAD", app, path, nil, headers...)
|
return c.Do("HEAD", app, path, nil, headers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 发起 GET 请求
|
|
||||||
func (d *Discoverer) Get(app, path string, headers ...string) *gohttp.Result {
|
|
||||||
return d.NewCaller(nil, nil).Get(app, path, headers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post 发起 POST 请求
|
|
||||||
func (d *Discoverer) Post(app, path string, data any, headers ...string) *gohttp.Result {
|
|
||||||
return d.NewCaller(nil, nil).Post(app, path, data, headers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put 发起 PUT 请求
|
|
||||||
func (d *Discoverer) Put(app, path string, data any, headers ...string) *gohttp.Result {
|
|
||||||
return d.NewCaller(nil, nil).Put(app, path, data, headers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete 发起 DELETE 请求
|
|
||||||
func (d *Discoverer) Delete(app, path string, data any, headers ...string) *gohttp.Result {
|
|
||||||
return d.NewCaller(nil, nil).Delete(app, path, data, headers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Head 发起 HEAD 请求
|
|
||||||
func (d *Discoverer) Head(app, path string, headers ...string) *gohttp.Result {
|
|
||||||
return d.NewCaller(nil, nil).Head(app, path, headers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do 发起通用请求
|
|
||||||
func (d *Discoverer) Do(method, app, path string, data any, headers ...string) *gohttp.Result {
|
|
||||||
return d.NewCaller(nil, nil).Do(method, app, path, data, headers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open 发起 WebSocket 连接
|
|
||||||
func (d *Discoverer) Open(app, path string, headers ...string) *websocket.Conn {
|
|
||||||
return d.NewCaller(nil, nil).Open(app, path, headers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do 发起通用请求
|
|
||||||
func (c *Caller) Do(method, app, path string, data any, headers ...string) *gohttp.Result {
|
func (c *Caller) Do(method, app, path string, data any, headers ...string) *gohttp.Result {
|
||||||
r, _ := c.DoWithNode(method, app, "", path, data, headers...)
|
r, _ := c.DoWithNode(method, app, "", path, data, headers...)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open 发起 WebSocket 连接
|
|
||||||
func (c *Caller) Open(app, path string, headers ...string) *websocket.Conn {
|
func (c *Caller) Open(app, path string, headers ...string) *websocket.Conn {
|
||||||
r, _ := c.doWithNode(false, "WS", app, "", path, nil, headers...)
|
r, _ := c.doWithNode(false, "WS", app, "", path, nil, headers...)
|
||||||
if v, ok := r.(*websocket.Conn); ok {
|
if v, ok := r.(*websocket.Conn); ok {
|
||||||
@ -137,7 +91,6 @@ func (c *Caller) Open(app, path string, headers ...string) *websocket.Conn {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoWithNode 发起请求并返回结果及节点地址
|
|
||||||
func (c *Caller) DoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
func (c *Caller) DoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
||||||
r, nodeAddr := c.doWithNode(false, method, app, withNode, path, data, headers...)
|
r, nodeAddr := c.doWithNode(false, method, app, withNode, path, data, headers...)
|
||||||
if v, ok := r.(*gohttp.Result); ok {
|
if v, ok := r.(*gohttp.Result); ok {
|
||||||
@ -146,7 +99,6 @@ func (c *Caller) DoWithNode(method, app, withNode, path string, data any, header
|
|||||||
return nil, nodeAddr
|
return nil, nodeAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManualDoWithNode 发起请求(手动处理响应)并返回结果及节点地址
|
|
||||||
func (c *Caller) ManualDoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
func (c *Caller) ManualDoWithNode(method, app, withNode, path string, data any, headers ...string) (*gohttp.Result, string) {
|
||||||
r, nodeAddr := c.doWithNode(true, method, app, withNode, path, data, headers...)
|
r, nodeAddr := c.doWithNode(true, method, app, withNode, path, data, headers...)
|
||||||
if v, ok := r.(*gohttp.Result); ok {
|
if v, ok := r.(*gohttp.Result); ok {
|
||||||
@ -155,15 +107,15 @@ func (c *Caller) ManualDoWithNode(method, app, withNode, path string, data any,
|
|||||||
return nil, nodeAddr
|
return nil, nodeAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, data any, headers ...string) (any, string) {
|
func (c *Caller) doWithNode(manualDo bool, method, app, withNode, path string, data any, headers ...string) (any, string) {
|
||||||
callerHeaders := make(map[string]string)
|
callerHeaders := make(map[string]string)
|
||||||
for i := 1; i < len(headers); i += 2 {
|
for i := 1; i < len(headers); i += 2 {
|
||||||
callerHeaders[headers[i-1]] = headers[i]
|
callerHeaders[headers[i-1]] = headers[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.discoverer.isServer {
|
if isServer {
|
||||||
callerHeaders[HeaderFromApp] = c.discoverer.app
|
callerHeaders[HeaderFromApp] = Config.App
|
||||||
callerHeaders[HeaderFromNode] = c.discoverer.myAddr
|
callerHeaders[HeaderFromNode] = myAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
callData := make(map[string]any)
|
callData := make(map[string]any)
|
||||||
@ -175,17 +127,16 @@ func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
appClient := AppClient{
|
appClient := AppClient{
|
||||||
discoverer: c.discoverer,
|
|
||||||
Logger: c.logger,
|
Logger: c.logger,
|
||||||
App: app,
|
App: app,
|
||||||
Method: method,
|
Method: method,
|
||||||
Path: path,
|
Path: path,
|
||||||
Data: callData,
|
Data: &callData,
|
||||||
Headers: callerHeaders,
|
Headers: &callerHeaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.discoverer.settedRoute != nil {
|
if settedRoute != nil {
|
||||||
c.discoverer.settedRoute(&appClient, c.Request)
|
settedRoute(&appClient, c.Request)
|
||||||
app = appClient.App
|
app = appClient.App
|
||||||
method = appClient.Method
|
method = appClient.Method
|
||||||
path = appClient.Path
|
path = appClient.Path
|
||||||
@ -195,11 +146,9 @@ func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, dat
|
|||||||
return &gohttp.Result{Error: fmt.Errorf("app %s not found", app)}, ""
|
return &gohttp.Result{Error: fmt.Errorf("app %s not found", app)}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
callInfo, hasCallInfo := c.discoverer.getCallInfo(app)
|
callInfo := getCallInfo(app)
|
||||||
if hasCallInfo && callInfo.Token != nil {
|
if callInfo != nil && callInfo.Token != "" {
|
||||||
tk := callInfo.Token.Open()
|
callerHeaders["Access-Token"] = callInfo.Token
|
||||||
callerHeaders["Access-Token"] = tk.String()
|
|
||||||
tk.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settedHeaders := make([]string, 0, len(callerHeaders)*2)
|
settedHeaders := make([]string, 0, len(callerHeaders)*2)
|
||||||
@ -213,23 +162,14 @@ func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, dat
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
node.UsedTimes.Add(1)
|
node.UsedTimes++
|
||||||
tracker := timer.Start()
|
startTime := time.Now()
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
if hasCallInfo && callInfo.SSL {
|
if callInfo != nil && callInfo.SSL {
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout := 10 * time.Second
|
hc := getHttpClient(app, callInfo.Timeout, callInfo.HttpVersion == 2 && !callInfo.SSL)
|
||||||
h2c := false
|
|
||||||
if hasCallInfo {
|
|
||||||
if callInfo.Timeout > 0 {
|
|
||||||
timeout = callInfo.Timeout
|
|
||||||
}
|
|
||||||
h2c = callInfo.Http2 && !callInfo.SSL
|
|
||||||
}
|
|
||||||
|
|
||||||
hc := c.discoverer.getHttpClient(app, timeout, h2c)
|
|
||||||
hc.NoBody = c.NoBody
|
hc.NoBody = c.NoBody
|
||||||
|
|
||||||
var res *gohttp.Result
|
var res *gohttp.Result
|
||||||
@ -254,13 +194,13 @@ func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, dat
|
|||||||
res = &gohttp.Result{Error: err, Response: resp}
|
res = &gohttp.Result{Error: err, Response: resp}
|
||||||
} else {
|
} else {
|
||||||
if c.Request != nil {
|
if c.Request != nil {
|
||||||
if manual {
|
if manualDo {
|
||||||
res = hc.ManualDoByRequest(c.Request, method, url, data, settedHeaders...)
|
res = hc.ManualDoByRequest(c.Request, method, url, data, settedHeaders...)
|
||||||
} else {
|
} else {
|
||||||
res = hc.DoByRequest(c.Request, method, url, data, settedHeaders...)
|
res = hc.DoByRequest(c.Request, method, url, data, settedHeaders...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if manual {
|
if manualDo {
|
||||||
res = hc.ManualDo(method, url, data, settedHeaders...)
|
res = hc.ManualDo(method, url, data, settedHeaders...)
|
||||||
} else {
|
} else {
|
||||||
res = hc.Do(method, url, data, settedHeaders...)
|
res = hc.Do(method, url, data, settedHeaders...)
|
||||||
@ -268,12 +208,11 @@ func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, dat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
responseTime := tracker.Record("call")
|
responseTime := time.Since(startTime)
|
||||||
usedTimeMs := float32(responseTime.Nanoseconds()) / 1e6
|
settedLoadBalancer.Response(&appClient, node, res.Error, res.Response, responseTime)
|
||||||
c.discoverer.settedLoadBalancer.Response(&appClient, node, res.Error, res.Response, responseTime)
|
|
||||||
|
|
||||||
if res.Error != nil || (res.Response != nil && res.Response.StatusCode >= 502 && res.Response.StatusCode <= 504) {
|
if res.Error != nil || (res.Response != nil && res.Response.StatusCode >= 502 && res.Response.StatusCode <= 504) {
|
||||||
node.FailedTimes.Add(1)
|
node.FailedTimes++
|
||||||
errStr := ""
|
errStr := ""
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
errStr = res.Error.Error()
|
errStr = res.Error.Error()
|
||||||
@ -281,17 +220,18 @@ func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, dat
|
|||||||
errStr = res.Response.Status
|
errStr = res.Response.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logError(errStr, "app", app, "node", node.Addr, "path", path, "attempts", appClient.attempts)
|
c.logError(errStr, "app", app, "node", node.Addr, "path", path, "tryTimes", appClient.tryTimes)
|
||||||
appClient.Log(node.Addr, usedTimeMs, fmt.Errorf("%s", errStr))
|
|
||||||
|
|
||||||
if node.FailedTimes.Load() >= int32(c.discoverer.config.CallRetryTimes) {
|
if node.FailedTimes >= Config.CallRetryTimes {
|
||||||
c.discoverer.logError("node isolated locally due to high failures", "app", app, "node", node.Addr)
|
logError("node removed due to high failures", "app", app, "node", node.Addr)
|
||||||
|
if clientRedisPool != nil {
|
||||||
|
clientRedisPool.Do("HDEL", app, node.Addr)
|
||||||
|
clientRedisPool.PUBLISH("CH_"+app, fmt.Sprintf("%s 0", node.Addr))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
node.FailedTimes.Store(0)
|
|
||||||
appClient.Log(node.Addr, usedTimeMs, nil)
|
|
||||||
if strings.ToUpper(method) == "WS" {
|
if strings.ToUpper(method) == "WS" {
|
||||||
return wsConn, node.Addr
|
return wsConn, node.Addr
|
||||||
}
|
}
|
||||||
@ -300,4 +240,3 @@ func (c *Caller) doWithNode(manual bool, method, app, withNode, path string, dat
|
|||||||
|
|
||||||
return &gohttp.Result{Error: fmt.Errorf("all nodes failed for %s %s", app, path)}, ""
|
return &gohttp.Result{Error: fmt.Errorf("all nodes failed for %s %s", app, path)}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
Config.go
25
Config.go
@ -1,21 +1,14 @@
|
|||||||
package discover
|
package discover
|
||||||
|
|
||||||
import (
|
// Config 存储发现服务的全局配置
|
||||||
"time"
|
var Config = struct {
|
||||||
"apigo.cc/go/safe"
|
Registry string // 注册中心地址,如 redis://:@127.0.0.1:6379/15
|
||||||
)
|
App string // 当前应用名称
|
||||||
|
|
||||||
// CallConfig 下游服务调用配置
|
|
||||||
type CallConfig struct {
|
|
||||||
Timeout time.Duration // 请求超时时间
|
|
||||||
Token *safe.SafeBuf // 访问凭证 (必须安全存储)
|
|
||||||
Http2 bool // 是否强制使用 HTTP/2 (H2C/H2)
|
|
||||||
SSL bool // 是否使用 HTTPS/WSS
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config 存储发现服务的可选配置
|
|
||||||
type Config struct {
|
|
||||||
Weight int // 权重,默认为 100
|
Weight int // 权重,默认为 100
|
||||||
Calls map[string]CallConfig // 调用的应用列表及其配置
|
Calls map[string]string // 调用的应用列表及其配置
|
||||||
CallRetryTimes int // 调用重试次数
|
CallRetryTimes int // 调用重试次数
|
||||||
|
IpPrefix string // 指定使用的 IP 网段
|
||||||
|
}{
|
||||||
|
Weight: 100,
|
||||||
|
CallRetryTimes: 10,
|
||||||
}
|
}
|
||||||
|
|||||||
44
Constants.go
44
Constants.go
@ -1,27 +1,35 @@
|
|||||||
package discover
|
package discover
|
||||||
|
|
||||||
import (
|
|
||||||
gohttp "apigo.cc/go/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HeaderFromApp = gohttp.HeaderFromApp
|
HeaderFromApp = "X-Discover-From-App"
|
||||||
HeaderFromNode = gohttp.HeaderFromNode
|
HeaderFromNode = "X-Discover-From-Node"
|
||||||
|
|
||||||
HeaderClientIP = gohttp.HeaderClientIP
|
HeaderClientIp = "X-Client-Ip"
|
||||||
HeaderForwardedFor = gohttp.HeaderForwardedFor
|
HeaderForwardedFor = "X-Forwarded-For"
|
||||||
HeaderUserID = gohttp.HeaderUserID
|
HeaderUserId = "X-User-Id"
|
||||||
HeaderDeviceID = gohttp.HeaderDeviceID
|
HeaderDeviceId = "X-Device-Id"
|
||||||
HeaderClientAppName = gohttp.HeaderClientAppName
|
HeaderClientAppName = "X-Client-App-Name"
|
||||||
HeaderClientAppVersion = gohttp.HeaderClientAppVersion
|
HeaderClientAppVersion = "X-Client-App-Version"
|
||||||
HeaderSessionID = gohttp.HeaderSessionID
|
HeaderSessionId = "X-Session-Id"
|
||||||
HeaderRequestID = gohttp.HeaderRequestID
|
HeaderRequestId = "X-Request-Id"
|
||||||
HeaderHost = gohttp.HeaderHost
|
HeaderHost = "X-Host"
|
||||||
HeaderScheme = gohttp.HeaderScheme
|
HeaderScheme = "X-Scheme"
|
||||||
HeaderUserAgent = gohttp.HeaderUserAgent
|
HeaderUserAgent = "User-Agent"
|
||||||
)
|
)
|
||||||
|
|
||||||
var RelayHeaders = gohttp.RelayHeaders
|
var RelayHeaders = []string{
|
||||||
|
HeaderClientIp,
|
||||||
|
HeaderForwardedFor,
|
||||||
|
HeaderUserId,
|
||||||
|
HeaderDeviceId,
|
||||||
|
HeaderClientAppName,
|
||||||
|
HeaderClientAppVersion,
|
||||||
|
HeaderSessionId,
|
||||||
|
HeaderRequestId,
|
||||||
|
HeaderHost,
|
||||||
|
HeaderScheme,
|
||||||
|
HeaderUserAgent,
|
||||||
|
}
|
||||||
|
|
||||||
const DefaultRegistry = "127.0.0.1:6379::15"
|
const DefaultRegistry = "127.0.0.1:6379::15"
|
||||||
const EnvRegistry = "DISCOVER_REGISTRY"
|
const EnvRegistry = "DISCOVER_REGISTRY"
|
||||||
|
|||||||
549
Discover.go
549
Discover.go
@ -2,223 +2,183 @@ package discover
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
gohttp "apigo.cc/go/http"
|
"apigo.cc/go/config"
|
||||||
"apigo.cc/go/id"
|
"apigo.cc/go/id"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"apigo.cc/go/redis"
|
"apigo.cc/go/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Discoverer 发现服务实例
|
var (
|
||||||
type Discoverer struct {
|
|
||||||
config Config
|
|
||||||
registry string
|
|
||||||
app string
|
|
||||||
|
|
||||||
serverRedisPool *redis.Redis
|
serverRedisPool *redis.Redis
|
||||||
clientRedisPool *redis.Redis
|
clientRedisPool *redis.Redis
|
||||||
pubsubRedisPool *redis.Redis
|
pubsubRedisPool *redis.Redis
|
||||||
isServer bool
|
isServer = false
|
||||||
isClient bool
|
isClient = false
|
||||||
daemonRunning atomic.Bool
|
daemonRunning = false
|
||||||
myAddr string
|
myAddr = ""
|
||||||
logger *log.Logger
|
_logger = log.DefaultLogger
|
||||||
daemonStopSignal chan struct{}
|
_inited = false
|
||||||
daemonDoneSignal chan struct{}
|
|
||||||
|
daemonStopChan chan bool
|
||||||
appLock sync.RWMutex
|
appLock sync.RWMutex
|
||||||
calls map[string]CallConfig
|
_calls = map[string]*callInfoType{}
|
||||||
appNodes map[string]map[string]*NodeInfo
|
_appNodes = map[string]map[string]*NodeInfo{}
|
||||||
appSubscribed map[string]bool
|
appSubscribed = map[string]bool{}
|
||||||
|
|
||||||
appClientPools map[string]*gohttp.Client
|
settedRoute func(*AppClient, *http.Request) = nil
|
||||||
appClientPoolsLock sync.RWMutex
|
settedLoadBalancer LoadBalancer = &DefaultLoadBalancer{}
|
||||||
|
)
|
||||||
|
|
||||||
settedRoute func(*AppClient, *http.Request)
|
type callInfoType struct {
|
||||||
settedLoadBalancer LoadBalancer
|
Timeout time.Duration
|
||||||
|
HttpVersion int
|
||||||
|
Token string
|
||||||
|
SSL bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsServer() bool { return isServer }
|
||||||
|
func IsClient() bool { return isClient }
|
||||||
|
|
||||||
|
func logError(error string, extra ...any) {
|
||||||
|
_logger.Error("Discover: "+error, append(extra, "app", Config.App, "addr", myAddr)...)
|
||||||
// IsServer 返回当前节点是否作为服务端运行
|
|
||||||
func (d *Discoverer) IsServer() bool { return d.isServer }
|
|
||||||
|
|
||||||
// IsClient 返回当前节点是否作为客户端运行
|
|
||||||
func (d *Discoverer) IsClient() bool { return d.isClient }
|
|
||||||
|
|
||||||
func (d *Discoverer) logError(msg string, extra ...any) {
|
|
||||||
d.logger.Error("Discover: "+msg, append(extra, "app", d.app, "addr", d.myAddr)...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) logInfo(msg string, extra ...any) {
|
func logInfo(info string, extra ...any) {
|
||||||
d.logger.Info("Discover: "+msg, append(extra, "app", d.app, "addr", d.myAddr)...)
|
_logger.Info("Discover: "+info, append(extra, "app", Config.App, "addr", myAddr)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogger 设置 Discover 使用的全局 Logger
|
func SetLogger(logger *log.Logger) {
|
||||||
func (d *Discoverer) SetLogger(logger *log.Logger) {
|
_logger = logger
|
||||||
d.logger = logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建一个新的发现服务实例
|
func Init() {
|
||||||
func New(logger *log.Logger, confs ...Config) *Discoverer {
|
appLock.Lock()
|
||||||
var conf Config
|
defer appLock.Unlock()
|
||||||
if len(confs) > 0 {
|
if _inited {
|
||||||
conf = confs[0]
|
return
|
||||||
}
|
}
|
||||||
if conf.CallRetryTimes <= 0 {
|
_inited = true
|
||||||
conf.CallRetryTimes = 10
|
_ = config.Load(&Config, "discover")
|
||||||
|
|
||||||
|
if Config.CallRetryTimes <= 0 {
|
||||||
|
Config.CallRetryTimes = 10
|
||||||
}
|
}
|
||||||
if conf.Weight <= 0 {
|
if Config.Weight <= 0 {
|
||||||
conf.Weight = 100
|
Config.Weight = 100
|
||||||
|
}
|
||||||
|
if Config.Registry == "" {
|
||||||
|
Config.Registry = DefaultRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
if logger == nil {
|
_logger = log.New(id.MakeID(12))
|
||||||
logger = log.DefaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
d := &Discoverer{
|
|
||||||
config: conf,
|
|
||||||
calls: make(map[string]CallConfig),
|
|
||||||
appNodes: make(map[string]map[string]*NodeInfo),
|
|
||||||
appSubscribed: make(map[string]bool),
|
|
||||||
appClientPools: make(map[string]*gohttp.Client),
|
|
||||||
settedLoadBalancer: &DefaultLoadBalancer{},
|
|
||||||
daemonStopSignal: make(chan struct{}),
|
|
||||||
daemonDoneSignal: make(chan struct{}),
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 启动服务发现,指定当前节点的外部访问地址
|
func Start(addr string) bool {
|
||||||
func Start(registry, app, addr string, logger *log.Logger, confs ...Config) *Discoverer {
|
Init()
|
||||||
d := New(logger, confs...)
|
myAddr = addr
|
||||||
d.registry = registry
|
|
||||||
d.app = app
|
|
||||||
d.myAddr = addr
|
|
||||||
|
|
||||||
d.isServer = d.app != "" && d.config.Weight > 0
|
isServer = Config.App != "" && Config.Weight > 0
|
||||||
if d.isServer && d.registry != "" {
|
if isServer && Config.Registry != "" {
|
||||||
d.serverRedisPool = redis.GetRedis(d.registry, d.logger)
|
serverRedisPool = redis.GetRedis(Config.Registry, _logger)
|
||||||
if d.serverRedisPool.Error != nil {
|
if serverRedisPool.Error != nil {
|
||||||
d.logError(d.serverRedisPool.Error.Error())
|
logError(serverRedisPool.Error.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册节点
|
// 注册节点
|
||||||
if d.serverRedisPool.Do("HSET", d.app, addr, d.config.Weight).Error == nil {
|
if serverRedisPool.Do("HSET", Config.App, addr, Config.Weight).Error == nil {
|
||||||
d.serverRedisPool.Do("SETEX", d.app+"_"+addr, 10, "1")
|
serverRedisPool.Do("SETEX", Config.App+"_"+addr, 10, "1")
|
||||||
d.logInfo("registered")
|
logInfo("registered")
|
||||||
d.serverRedisPool.PUBLISH("CH_"+d.app, fmt.Sprintf("%s %d", addr, d.config.Weight))
|
serverRedisPool.PUBLISH("CH_"+Config.App, fmt.Sprintf("%s %d", addr, Config.Weight))
|
||||||
d.daemonRunning.Store(true)
|
daemonRunning = true
|
||||||
d.daemonStopSignal = make(chan struct{})
|
daemonStopChan = make(chan bool)
|
||||||
d.daemonDoneSignal = make(chan struct{})
|
go daemon()
|
||||||
go d.daemon()
|
|
||||||
} else {
|
} else {
|
||||||
d.logError("register failed")
|
logError("register failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
calls := d.config.Calls
|
calls := getCalls()
|
||||||
if len(calls) > 0 {
|
if len(calls) > 0 {
|
||||||
for callApp, c := range calls {
|
for app, conf := range calls {
|
||||||
d.addApp(callApp, c, false)
|
addApp(app, conf, false)
|
||||||
}
|
}
|
||||||
if !d.startSub() {
|
if !startSub() {
|
||||||
return d
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open 启动服务发现仅作为客户端
|
func daemon() {
|
||||||
func Open(registry string, logger *log.Logger, confs ...Config) *Discoverer {
|
logInfo("daemon thread started")
|
||||||
d := New(logger, confs...)
|
ticker := time.NewTicker(time.Second)
|
||||||
d.registry = registry
|
|
||||||
calls := d.config.Calls
|
|
||||||
if len(calls) > 0 {
|
|
||||||
for callApp, c := range calls {
|
|
||||||
d.addApp(callApp, c, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.startSub()
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func (d *Discoverer) daemon() {
|
|
||||||
d.logInfo("daemon thread started")
|
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for d.daemonRunning.Load() {
|
for daemonRunning {
|
||||||
select {
|
<-ticker.C
|
||||||
case <-ticker.C:
|
if !daemonRunning {
|
||||||
if !d.daemonRunning.Load() {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.isServer && d.serverRedisPool != nil {
|
if isServer && serverRedisPool != nil {
|
||||||
if !d.serverRedisPool.Do("HEXISTS", d.app, d.myAddr).Bool() {
|
if !serverRedisPool.Do("HEXISTS", Config.App, myAddr).Bool() {
|
||||||
d.logInfo("lost app registered info, re-registering")
|
logInfo("lost app registered info, re-registering")
|
||||||
if d.serverRedisPool.Do("HSET", d.app, d.myAddr, d.config.Weight).Error == nil {
|
if serverRedisPool.Do("HSET", Config.App, myAddr, Config.Weight).Error == nil {
|
||||||
d.serverRedisPool.Do("SETEX", d.app+"_"+d.myAddr, 10, "1")
|
serverRedisPool.Do("SETEX", Config.App+"_"+myAddr, 10, "1")
|
||||||
d.serverRedisPool.PUBLISH("CH_"+d.app, fmt.Sprintf("%s %d", d.myAddr, d.config.Weight))
|
serverRedisPool.PUBLISH("CH_"+Config.App, fmt.Sprintf("%s %d", myAddr, Config.Weight))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
d.serverRedisPool.Do("SETEX", d.app+"_"+d.myAddr, 10, "1")
|
serverRedisPool.Do("SETEX", Config.App+"_"+myAddr, 10, "1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case <-d.daemonStopSignal:
|
|
||||||
goto done
|
|
||||||
}
|
}
|
||||||
|
logInfo("daemon thread stopped")
|
||||||
|
if daemonStopChan != nil {
|
||||||
|
daemonStopChan <- true
|
||||||
}
|
}
|
||||||
done:
|
|
||||||
d.logInfo("daemon thread stopped")
|
|
||||||
close(d.daemonDoneSignal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) startSub() bool {
|
func startSub() bool {
|
||||||
if d.registry == "" {
|
if Config.Registry == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
d.appLock.Lock()
|
appLock.Lock()
|
||||||
if d.clientRedisPool == nil {
|
if clientRedisPool == nil {
|
||||||
d.clientRedisPool = redis.GetRedis(d.registry, d.logger)
|
clientRedisPool = redis.GetRedis(Config.Registry, _logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.pubsubRedisPool == nil {
|
if pubsubRedisPool == nil {
|
||||||
d.pubsubRedisPool = redis.GetRedis(d.registry, d.logger.New(id.MakeID(12)))
|
pubsubRedisPool = redis.GetRedis(Config.Registry, _logger.New(id.MakeID(12)))
|
||||||
// 订阅所有已注册的应用
|
// 订阅所有已注册的应用
|
||||||
for app := range d.appSubscribed {
|
for app := range appSubscribed {
|
||||||
d.subscribeApp(app)
|
subscribeAppUnderLock(app)
|
||||||
}
|
}
|
||||||
// 必须在释放锁之前完成配置,但在释放锁之后启动,避免死锁
|
// 必须在释放锁之前完成配置,但在释放锁之后启动,避免死锁
|
||||||
d.appLock.Unlock()
|
appLock.Unlock()
|
||||||
d.pubsubRedisPool.Start()
|
pubsubRedisPool.Start()
|
||||||
d.appLock.Lock()
|
appLock.Lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
d.isClient = true
|
isClient = true
|
||||||
d.appLock.Unlock()
|
appLock.Unlock()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) subscribeApp(app string) {
|
func subscribeAppUnderLock(app string) {
|
||||||
if d.pubsubRedisPool == nil {
|
pubsubRedisPool.Subscribe("CH_"+app, func() {
|
||||||
d.appSubscribed[app] = true
|
fetchApp(app)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d.pubsubRedisPool.Subscribe("CH_"+app, func() {
|
|
||||||
d.fetchApp(app)
|
|
||||||
}, func(data []byte) {
|
}, func(data []byte) {
|
||||||
a := strings.Split(string(data), " ")
|
a := strings.Split(string(data), " ")
|
||||||
addr := a[0]
|
addr := a[0]
|
||||||
@ -226,136 +186,176 @@ func (d *Discoverer) subscribeApp(app string) {
|
|||||||
if len(a) == 2 {
|
if len(a) == 2 {
|
||||||
weight = cast.Int(a[1])
|
weight = cast.Int(a[1])
|
||||||
}
|
}
|
||||||
d.logInfo("received node update", "app", app, "addr", addr, "weight", weight)
|
logInfo("received node update", "app", app, "addr", addr, "weight", weight)
|
||||||
d.pushNode(app, addr, weight)
|
pushNode(app, addr, weight)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) subscribeAppWithLock(app string) {
|
func Stop() {
|
||||||
d.appLock.Lock()
|
appLock.Lock()
|
||||||
defer d.appLock.Unlock()
|
if isClient && pubsubRedisPool != nil {
|
||||||
d.subscribeApp(app)
|
pubsubRedisPool.Stop()
|
||||||
}
|
isClient = false
|
||||||
|
}
|
||||||
// Stop 停止 Discover 并从注册中心注销当前节点
|
|
||||||
func (d *Discoverer) Stop() {
|
|
||||||
d.appLock.Lock()
|
|
||||||
|
|
||||||
// 1. 提取需要的状态,提前修改标志位
|
|
||||||
isClient := d.isClient
|
|
||||||
pubsub := d.pubsubRedisPool
|
|
||||||
d.isClient = false
|
|
||||||
|
|
||||||
isServer := d.isServer
|
|
||||||
serverPool := d.serverRedisPool
|
|
||||||
myAddr := d.myAddr
|
|
||||||
|
|
||||||
if isServer {
|
if isServer {
|
||||||
d.daemonRunning.Store(false)
|
daemonRunning = false
|
||||||
if d.daemonStopSignal != nil {
|
if serverRedisPool != nil {
|
||||||
close(d.daemonStopSignal)
|
serverRedisPool.Do("HDEL", Config.App, myAddr)
|
||||||
|
serverRedisPool.Do("DEL", Config.App+"_"+myAddr)
|
||||||
|
serverRedisPool.PUBLISH("CH_"+Config.App, fmt.Sprintf("%s %d", myAddr, 0))
|
||||||
}
|
}
|
||||||
d.isServer = false
|
isServer = false
|
||||||
}
|
}
|
||||||
|
appLock.Unlock()
|
||||||
// 核心修复:在这里尽早释放锁!避免与 pushNode 回调发生死锁
|
|
||||||
d.appLock.Unlock()
|
|
||||||
|
|
||||||
// 2. 在无锁状态下进行耗时的网络和停止操作
|
|
||||||
if isClient && pubsub != nil {
|
|
||||||
pubsub.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if isServer && serverPool != nil {
|
|
||||||
serverPool.Do("HDEL", d.app, myAddr)
|
|
||||||
serverPool.Do("DEL", d.app+"_"+myAddr)
|
|
||||||
serverPool.PUBLISH("CH_"+d.app, fmt.Sprintf("%s %d", myAddr, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 释放 HTTP 连接池
|
|
||||||
d.appClientPoolsLock.Lock()
|
|
||||||
for _, client := range d.appClientPools {
|
|
||||||
client.Destroy()
|
|
||||||
}
|
|
||||||
d.appClientPools = make(map[string]*gohttp.Client)
|
|
||||||
d.appClientPoolsLock.Unlock()
|
|
||||||
|
|
||||||
// 4. 重置状态以支持重新启动
|
|
||||||
d.appLock.Lock()
|
|
||||||
d.serverRedisPool = nil
|
|
||||||
d.clientRedisPool = nil
|
|
||||||
d.pubsubRedisPool = nil
|
|
||||||
d.appNodes = make(map[string]map[string]*NodeInfo)
|
|
||||||
d.appSubscribed = make(map[string]bool)
|
|
||||||
d.appLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait 等待守护进程退出
|
func Wait() {
|
||||||
func (d *Discoverer) Wait() {
|
if daemonStopChan != nil {
|
||||||
if d.daemonDoneSignal != nil {
|
<-daemonStopChan
|
||||||
<-d.daemonDoneSignal
|
daemonStopChan = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EasyStart() (string, int) {
|
||||||
// AddExternalApp 动态添加需要发现的外部应用
|
Init()
|
||||||
func (d *Discoverer) AddExternalApp(app string, callConf CallConfig) bool {
|
port := 0
|
||||||
if d.addApp(app, callConf, true) {
|
if listen := os.Getenv("DISCOVER_LISTEN"); listen != "" {
|
||||||
if !d.isClient {
|
if _, p, err := net.SplitHostPort(listen); err == nil {
|
||||||
d.startSub()
|
port = cast.Int(p)
|
||||||
} else {
|
} else {
|
||||||
d.subscribeAppWithLock(app)
|
port = cast.Int(listen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||||
|
if err != nil {
|
||||||
|
logError("failed to listen", "err", err)
|
||||||
|
return "", 0
|
||||||
|
}
|
||||||
|
addrInfo := ln.Addr().(*net.TCPAddr)
|
||||||
|
_ = ln.Close()
|
||||||
|
port = addrInfo.Port
|
||||||
|
|
||||||
|
ip := addrInfo.IP
|
||||||
|
if !ip.IsGlobalUnicast() {
|
||||||
|
addrs, _ := net.InterfaceAddrs()
|
||||||
|
for _, a := range addrs {
|
||||||
|
if an, ok := a.(*net.IPNet); ok {
|
||||||
|
ip4 := an.IP.To4()
|
||||||
|
if ip4 == nil || !ip4.IsGlobalUnicast() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if Config.IpPrefix != "" && strings.HasPrefix(ip4.String(), Config.IpPrefix) {
|
||||||
|
ip = ip4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(ip4.String(), "172.17.") {
|
||||||
|
ip = ip4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", ip.String(), port)
|
||||||
|
if !Start(addr) {
|
||||||
|
return "", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ip.String(), port
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddExternalApp(app, callConf string) bool {
|
||||||
|
if addApp(app, callConf, true) {
|
||||||
|
if !isClient {
|
||||||
|
startSub()
|
||||||
|
} else {
|
||||||
|
appLock.Lock()
|
||||||
|
subscribeAppUnderLock(app)
|
||||||
|
appLock.Unlock()
|
||||||
}
|
}
|
||||||
d.fetchApp(app) // 同步拉取一次
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) getCallInfo(app string) (CallConfig, bool) {
|
func SetNode(app, addr string, weight int) {
|
||||||
d.appLock.RLock()
|
pushNode(app, addr, weight)
|
||||||
defer d.appLock.RUnlock()
|
|
||||||
info, exists := d.calls[app]
|
|
||||||
return info, exists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) addApp(app string, callConf CallConfig, fetch bool) bool {
|
func getCallInfo(app string) *callInfoType {
|
||||||
d.appLock.Lock()
|
appLock.RLock()
|
||||||
|
defer appLock.RUnlock()
|
||||||
|
return _calls[app]
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 写时复制(Copy-on-Write):创建一个全新的 Map 避免影响读操作
|
var numberMatcher = regexp.MustCompile(`^\d+(s|ms|us|µs|ns?)?$`)
|
||||||
newCalls := make(map[string]CallConfig)
|
|
||||||
for k, v := range d.config.Calls {
|
func addApp(app, callConf string, fetch bool) bool {
|
||||||
newCalls[k] = v
|
appLock.Lock()
|
||||||
|
if Config.Calls == nil {
|
||||||
|
Config.Calls = make(map[string]string)
|
||||||
|
}
|
||||||
|
if Config.Calls[app] == callConf && _appNodes[app] != nil {
|
||||||
|
appLock.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Config.Calls[app] = callConf
|
||||||
|
|
||||||
|
callInfo := &callInfoType{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
HttpVersion: 2,
|
||||||
|
SSL: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if existing, ok := newCalls[app]; ok {
|
for _, v := range cast.Split(callConf, ":") {
|
||||||
// compare? simple enough to just overwrite if we want to be safe, but let's check basic equality or just overwrite
|
switch v {
|
||||||
_ = existing
|
case "1":
|
||||||
|
callInfo.HttpVersion = 1
|
||||||
|
case "2":
|
||||||
|
callInfo.HttpVersion = 2
|
||||||
|
case "s", "https":
|
||||||
|
callInfo.SSL = true
|
||||||
|
callInfo.HttpVersion = 2
|
||||||
|
case "http":
|
||||||
|
callInfo.SSL = false
|
||||||
|
callInfo.HttpVersion = 1
|
||||||
|
case "h2c":
|
||||||
|
callInfo.SSL = false
|
||||||
|
callInfo.HttpVersion = 2
|
||||||
|
default:
|
||||||
|
if numberMatcher.MatchString(v) {
|
||||||
|
callInfo.Timeout = cast.Duration(v)
|
||||||
|
} else {
|
||||||
|
callInfo.Token = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if d.appNodes[app] != nil {
|
|
||||||
// If nodes exist, we might just be updating config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newCalls[app] = callConf
|
_calls[app] = callInfo
|
||||||
d.config.Calls = newCalls // 将新的 Map 赋值给 Config
|
if _appNodes[app] == nil {
|
||||||
d.calls[app] = callConf
|
_appNodes[app] = make(map[string]*NodeInfo)
|
||||||
|
|
||||||
if d.appNodes[app] == nil {
|
|
||||||
d.appNodes[app] = make(map[string]*NodeInfo)
|
|
||||||
}
|
}
|
||||||
d.appSubscribed[app] = true
|
appSubscribed[app] = true
|
||||||
d.appLock.Unlock()
|
appLock.Unlock()
|
||||||
|
|
||||||
if fetch && d.isClient {
|
if fetch {
|
||||||
d.fetchApp(app)
|
fetchApp(app)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) fetchApp(app string) {
|
func fetchApp(app string) {
|
||||||
d.appLock.RLock()
|
appLock.RLock()
|
||||||
pool := d.clientRedisPool
|
pool := clientRedisPool
|
||||||
d.appLock.RUnlock()
|
appLock.RUnlock()
|
||||||
if pool == nil {
|
if pool == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -370,79 +370,80 @@ func (d *Discoverer) fetchApp(app string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentNodes := d.getAppNodes(app)
|
currentNodes := getAppNodes(app)
|
||||||
if currentNodes != nil {
|
if currentNodes != nil {
|
||||||
for addr := range currentNodes {
|
for addr := range currentNodes {
|
||||||
if _, ok := results[addr]; !ok {
|
if _, ok := results[addr]; !ok {
|
||||||
d.pushNode(app, addr, 0)
|
pushNode(app, addr, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for addr, res := range results {
|
for addr, res := range results {
|
||||||
d.pushNode(app, addr, res.Int())
|
pushNode(app, addr, res.Int())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) getAppNodes(app string) map[string]*NodeInfo {
|
func getAppNodes(app string) map[string]*NodeInfo {
|
||||||
d.appLock.RLock()
|
appLock.RLock()
|
||||||
defer d.appLock.RUnlock()
|
defer appLock.RUnlock()
|
||||||
if d.appNodes[app] == nil {
|
if _appNodes[app] == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
nodes := make(map[string]*NodeInfo)
|
nodes := make(map[string]*NodeInfo)
|
||||||
for k, v := range d.appNodes[app] {
|
for k, v := range _appNodes[app] {
|
||||||
nodes[k] = v
|
nodes[k] = v
|
||||||
}
|
}
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNode 手动设置某个服务的节点信息
|
func getCalls() map[string]string {
|
||||||
func (d *Discoverer) SetNode(app, addr string, weight int) {
|
appLock.RLock()
|
||||||
d.pushNode(app, addr, weight)
|
defer appLock.RUnlock()
|
||||||
|
calls := make(map[string]string)
|
||||||
|
for k, v := range Config.Calls {
|
||||||
|
calls[k] = v
|
||||||
|
}
|
||||||
|
return calls
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAppNodes 获取某个应用的所有节点列表
|
func GetAppNodes(app string) map[string]*NodeInfo {
|
||||||
func (d *Discoverer) GetAppNodes(app string) map[string]*NodeInfo {
|
return getAppNodes(app)
|
||||||
return d.getAppNodes(app)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discoverer) pushNode(app, addr string, weight int) {
|
func pushNode(app, addr string, weight int) {
|
||||||
d.appLock.Lock()
|
appLock.Lock()
|
||||||
defer d.appLock.Unlock()
|
defer appLock.Unlock()
|
||||||
|
|
||||||
if weight <= 0 {
|
if weight <= 0 {
|
||||||
if d.appNodes[app] != nil {
|
if _appNodes[app] != nil {
|
||||||
delete(d.appNodes[app], addr)
|
delete(_appNodes[app], addr)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.appNodes[app] == nil {
|
if _appNodes[app] == nil {
|
||||||
d.appNodes[app] = make(map[string]*NodeInfo)
|
_appNodes[app] = make(map[string]*NodeInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if node, ok := d.appNodes[app][addr]; ok {
|
if node, ok := _appNodes[app][addr]; ok {
|
||||||
if node.Weight != weight {
|
if node.Weight != weight {
|
||||||
used := node.UsedTimes.Load()
|
node.UsedTimes = uint64(float64(node.UsedTimes) / float64(node.Weight) * float64(weight))
|
||||||
node.UsedTimes.Store(uint64(float64(used) / float64(node.Weight) * float64(weight)))
|
|
||||||
node.Weight = weight
|
node.Weight = weight
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var avgUsed uint64 = 0
|
var avgUsed uint64 = 0
|
||||||
if len(d.appNodes[app]) > 0 {
|
if len(_appNodes[app]) > 0 {
|
||||||
var totalScore float64
|
var totalScore float64
|
||||||
for _, n := range d.appNodes[app] {
|
for _, n := range _appNodes[app] {
|
||||||
totalScore += float64(n.UsedTimes.Load()) / float64(n.Weight)
|
totalScore += float64(n.UsedTimes) / float64(n.Weight)
|
||||||
}
|
}
|
||||||
avgUsed = uint64(totalScore / float64(len(d.appNodes[app])) * float64(weight))
|
avgUsed = uint64(totalScore / float64(len(_appNodes[app])) * float64(weight))
|
||||||
}
|
}
|
||||||
node := &NodeInfo{
|
_appNodes[app][addr] = &NodeInfo{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Weight: weight,
|
Weight: weight,
|
||||||
|
UsedTimes: avgUsed,
|
||||||
}
|
}
|
||||||
node.UsedTimes.Store(avgUsed)
|
|
||||||
d.appNodes[app][addr] = node
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package discover_test
|
package discover_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -13,12 +15,11 @@ import (
|
|||||||
|
|
||||||
func TestDiscover(t *testing.T) {
|
func TestDiscover(t *testing.T) {
|
||||||
// 启动一个模拟服务
|
// 启动一个模拟服务
|
||||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
l, err := net.Listen("tcp", "127.0.0.1:18001")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("failed to listen, skipping test")
|
t.Skip("failed to listen on :18001, skipping test")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
addr := l.Addr().String()
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("OK"))
|
_, _ = w.Write([]byte("OK"))
|
||||||
@ -44,21 +45,24 @@ func TestDiscover(t *testing.T) {
|
|||||||
go func() { _ = server.Serve(l) }()
|
go func() { _ = server.Serve(l) }()
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
// 配置 Discover
|
||||||
|
discover.Config.App = "test-app"
|
||||||
|
discover.Config.Registry = "redis://127.0.0.1:6379/15"
|
||||||
|
|
||||||
// 启动 Discover
|
// 启动 Discover
|
||||||
d := discover.Start("redis://127.0.0.1:6379/15", "test-app", addr, nil)
|
if !discover.Start("127.0.0.1:18001") {
|
||||||
if d == nil {
|
|
||||||
t.Skip("failed to start discover (check redis), skipping test")
|
t.Skip("failed to start discover (check redis), skipping test")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer d.Stop()
|
defer discover.Stop()
|
||||||
|
|
||||||
// 添加外部应用调用配置
|
// 添加外部应用调用配置
|
||||||
d.AddExternalApp("test-app", discover.CallConfig{Timeout: time.Second})
|
discover.AddExternalApp("test-app", "1")
|
||||||
|
|
||||||
// 等待节点同步
|
// 等待节点同步
|
||||||
success := false
|
success := false
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
nodes := d.GetAppNodes("test-app")
|
nodes := discover.GetAppNodes("test-app")
|
||||||
if len(nodes) > 0 {
|
if len(nodes) > 0 {
|
||||||
success = true
|
success = true
|
||||||
break
|
break
|
||||||
@ -70,7 +74,7 @@ func TestDiscover(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. 使用 Caller 调用 HTTP
|
// 1. 使用 Caller 调用 HTTP
|
||||||
caller := d.NewCaller(nil, nil)
|
caller := discover.NewCaller(nil, nil)
|
||||||
res := caller.Get("test-app", "/")
|
res := caller.Get("test-app", "/")
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
t.Errorf("http call failed: %v", res.Error)
|
t.Errorf("http call failed: %v", res.Error)
|
||||||
@ -100,14 +104,14 @@ func TestDiscover(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 测试负载均衡和节点更新
|
// 3. 测试负载均衡和节点更新
|
||||||
rd := redis.GetRedis("redis://127.0.0.1:6379/15", nil)
|
rd := redis.GetRedis(discover.Config.Registry, nil)
|
||||||
if rd.Error == nil {
|
if rd.Error == nil {
|
||||||
// 模拟发现新节点
|
// 模拟发现新节点
|
||||||
rd.PUBLISH("CH_test-app", "127.0.0.1:18002 100")
|
rd.PUBLISH("CH_test-app", "127.0.0.1:18002 100")
|
||||||
|
|
||||||
success = false
|
success = false
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
nodes := d.GetAppNodes("test-app")
|
nodes := discover.GetAppNodes("test-app")
|
||||||
if len(nodes) >= 2 {
|
if len(nodes) >= 2 {
|
||||||
success = true
|
success = true
|
||||||
break
|
break
|
||||||
@ -120,16 +124,17 @@ func TestDiscover(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkDiscover(b *testing.B) {
|
func TestEasyStart(t *testing.T) {
|
||||||
d := discover.New(nil)
|
// 模拟环境变量
|
||||||
d.SetNode("bench-app", "127.0.0.1:8080", 100)
|
_ = os.Setenv("DISCOVER_APP", "test-app")
|
||||||
d.SetNode("bench-app", "127.0.0.1:8081", 100)
|
_ = os.Setenv("DISCOVER_LISTEN", "18003")
|
||||||
|
_ = os.Setenv("DISCOVER_REGISTRY", "redis://127.0.0.1:6379/15")
|
||||||
|
|
||||||
b.ResetTimer()
|
ip, port := discover.EasyStart()
|
||||||
for i := 0; i < b.N; i++ {
|
if ip == "" || port == 0 {
|
||||||
nodes := d.GetAppNodes("bench-app")
|
t.Skip("EasyStart failed (check redis), skipping test")
|
||||||
if len(nodes) == 0 {
|
return
|
||||||
b.Fatal("no node")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("EasyStart: %s:%d\n", ip, port)
|
||||||
|
discover.Stop()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetLoadBalancer 设置负载均衡策略
|
// SetLoadBalancer 设置全局负载均衡策略
|
||||||
func (d *Discoverer) SetLoadBalancer(lb LoadBalancer) {
|
func SetLoadBalancer(lb LoadBalancer) {
|
||||||
d.settedLoadBalancer = lb
|
settedLoadBalancer = lb
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadBalancer 负载均衡接口
|
// LoadBalancer 负载均衡接口
|
||||||
@ -22,17 +22,20 @@ type LoadBalancer interface {
|
|||||||
// DefaultLoadBalancer 默认负载均衡器(简单权重轮询/得分最小者优先)
|
// DefaultLoadBalancer 默认负载均衡器(简单权重轮询/得分最小者优先)
|
||||||
type DefaultLoadBalancer struct{}
|
type DefaultLoadBalancer struct{}
|
||||||
|
|
||||||
// Response 在默认负载均衡器中不再执行写操作,减少锁竞争
|
|
||||||
func (lb *DefaultLoadBalancer) Response(appClient *AppClient, node *NodeInfo, err error, response *http.Response, responseTime time.Duration) {
|
func (lb *DefaultLoadBalancer) Response(appClient *AppClient, node *NodeInfo, err error, response *http.Response, responseTime time.Duration) {
|
||||||
|
node.Data.Store("score", float64(node.UsedTimes)/float64(node.Weight))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next 根据得分(UsedTimes / Weight)选择得分最小的节点
|
|
||||||
func (lb *DefaultLoadBalancer) Next(appClient *AppClient, nodes []*NodeInfo, request *http.Request) *NodeInfo {
|
func (lb *DefaultLoadBalancer) Next(appClient *AppClient, nodes []*NodeInfo, request *http.Request) *NodeInfo {
|
||||||
var minScore float64 = -1
|
var minScore float64 = -1
|
||||||
var minNode *NodeInfo
|
var minNode *NodeInfo
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
// 动态计算得分,避免使用 sync.Map 存储,减少内存分配和锁竞争
|
scoreValue, ok := node.Data.Load("score")
|
||||||
score := float64(node.UsedTimes.Load()) / float64(node.Weight)
|
if !ok {
|
||||||
|
scoreValue = float64(node.UsedTimes) / float64(node.Weight)
|
||||||
|
node.Data.Store("score", scoreValue)
|
||||||
|
}
|
||||||
|
score := scoreValue.(float64)
|
||||||
if minNode == nil || score < minScore {
|
if minNode == nil || score < minScore {
|
||||||
minScore = score
|
minScore = score
|
||||||
minNode = node
|
minNode = node
|
||||||
|
|||||||
55
Log.go
55
Log.go
@ -1,55 +0,0 @@
|
|||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"apigo.cc/go/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const LogTypeDiscover = "discover"
|
|
||||||
|
|
||||||
type DiscoverLog struct {
|
|
||||||
log.BaseLog
|
|
||||||
App string `log:"pos:6,color:cyan"`
|
|
||||||
Method string `log:"pos:7,color:magenta"`
|
|
||||||
Path string `log:"pos:8,color:blue"`
|
|
||||||
Node string `log:"pos:9,color:yellow"`
|
|
||||||
Attempts int `log:"pos:10"`
|
|
||||||
UsedTime float32 `log:"pos:11,format:%.2fms"`
|
|
||||||
Error string `log:"pos:12,color:red"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *DiscoverLog) Reset() {
|
|
||||||
l.BaseLog.Reset()
|
|
||||||
l.App = ""
|
|
||||||
l.Method = ""
|
|
||||||
l.Path = ""
|
|
||||||
l.Node = ""
|
|
||||||
l.Attempts = 0
|
|
||||||
l.UsedTime = 0
|
|
||||||
l.Error = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.RegisterType(LogTypeDiscover, DiscoverLog{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *AppClient) Log(node string, usedTime float32, err error) {
|
|
||||||
if ac.Logger == nil {
|
|
||||||
ac.Logger = log.DefaultLogger
|
|
||||||
}
|
|
||||||
if !ac.Logger.CheckLevel(log.INFO) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := log.GetEntry[DiscoverLog]()
|
|
||||||
// 框架会自动调用 fillBase,只需填充业务字段
|
|
||||||
entry.App = ac.App
|
|
||||||
entry.Method = ac.Method
|
|
||||||
entry.Path = ac.Path
|
|
||||||
entry.Node = node
|
|
||||||
entry.Attempts = ac.attempts
|
|
||||||
entry.UsedTime = usedTime
|
|
||||||
if err != nil {
|
|
||||||
entry.Error = err.Error()
|
|
||||||
}
|
|
||||||
ac.Logger.Log(entry)
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
package discover_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/discover"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMultipleDiscoverer(t *testing.T) {
|
|
||||||
// 启动两个模拟服务
|
|
||||||
l1, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen l1: %v", err)
|
|
||||||
}
|
|
||||||
addr1 := l1.Addr().String()
|
|
||||||
mux1 := http.NewServeMux()
|
|
||||||
mux1.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("OK1")) })
|
|
||||||
server1 := &http.Server{Handler: mux1}
|
|
||||||
go func() { _ = server1.Serve(l1) }()
|
|
||||||
defer server1.Close()
|
|
||||||
|
|
||||||
l2, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to listen l2: %v", err)
|
|
||||||
}
|
|
||||||
addr2 := l2.Addr().String()
|
|
||||||
mux2 := http.NewServeMux()
|
|
||||||
mux2.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("OK2")) })
|
|
||||||
server2 := &http.Server{Handler: mux2}
|
|
||||||
go func() { _ = server2.Serve(l2) }()
|
|
||||||
defer server2.Close()
|
|
||||||
|
|
||||||
registry := "redis://127.0.0.1:6379/15"
|
|
||||||
|
|
||||||
// 实例 1
|
|
||||||
d1 := discover.Start(registry + "?id=1", "app1", addr1, nil)
|
|
||||||
if d1 == nil {
|
|
||||||
t.Skip("redis not available")
|
|
||||||
}
|
|
||||||
defer d1.Stop()
|
|
||||||
|
|
||||||
// 实例 2
|
|
||||||
d2 := discover.Start(registry + "?id=2", "app2", addr2, nil)
|
|
||||||
if d2 == nil {
|
|
||||||
t.Skip("redis not available")
|
|
||||||
}
|
|
||||||
defer d2.Stop()
|
|
||||||
|
|
||||||
// 实例 1 发现并调用自己
|
|
||||||
d1.AddExternalApp("app1", discover.CallConfig{})
|
|
||||||
time.Sleep(200 * time.Millisecond) // 等待同步
|
|
||||||
c1 := d1.NewCaller(nil, nil)
|
|
||||||
res1 := c1.Get("app1", "/")
|
|
||||||
if res1.Error != nil || res1.String() != "OK1" {
|
|
||||||
t.Errorf("d1 call app1 failed: %v, %s", res1.Error, res1.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实例 2 发现并调用 实例 1
|
|
||||||
d2.AddExternalApp("app1", discover.CallConfig{})
|
|
||||||
time.Sleep(200 * time.Millisecond) // 等待同步
|
|
||||||
c2 := d2.NewCaller(nil, nil)
|
|
||||||
res2 := c2.Get("app1", "/")
|
|
||||||
if res2.Error != nil || res2.String() != "OK1" {
|
|
||||||
t.Errorf("d2 call app1 failed: %v, %s", res2.Error, res2.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证:d1 也可以调用 app2,只要正确配置
|
|
||||||
d1.AddExternalApp("app2", discover.CallConfig{})
|
|
||||||
time.Sleep(200 * time.Millisecond) // 等待同步
|
|
||||||
res3 := c1.Get("app2", "/")
|
|
||||||
if res3.Error != nil || res3.String() != "OK2" {
|
|
||||||
t.Errorf("d1 call app2 failed: %v, %s", res3.Error, res3.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Multiple Discoverer instances verified")
|
|
||||||
}
|
|
||||||
@ -2,14 +2,13 @@ package discover
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NodeInfo 存储服务节点信息
|
// NodeInfo 存储服务节点信息
|
||||||
type NodeInfo struct {
|
type NodeInfo struct {
|
||||||
Addr string // 节点地址
|
Addr string // 节点地址
|
||||||
Weight int // 节点权重
|
Weight int // 节点权重
|
||||||
UsedTimes atomic.Uint64 // 已使用次数
|
UsedTimes uint64 // 已使用次数
|
||||||
FailedTimes atomic.Int32 // 失败次数
|
FailedTimes int // 失败次数
|
||||||
Data sync.Map // 运行时自定义数据
|
Data sync.Map // 运行时自定义数据
|
||||||
}
|
}
|
||||||
|
|||||||
142
README.md
142
README.md
@ -1,114 +1,48 @@
|
|||||||
# @go/discover
|
# Discover
|
||||||
|
|
||||||
> **Maintainer Statement:** 本项目完全由 AI 维护。任何改动均遵循代码质量与性能的最佳实践。
|
基于 Redis 的极简服务发现与负载均衡组件。
|
||||||
|
|
||||||
`@go/discover` 是一个**无状态、参数驱动**的极简服务发现与负载均衡组件。它基于 Redis 实现,专注于消除微服务调用间的摩擦,并原生支持 Header 链路透传。
|
## 核心特性
|
||||||
|
- **自动注册与发现**: 基于 Redis 的服务节点自动注册、心跳维持及实时更新。
|
||||||
|
- **智能负载均衡**: 支持按权重分配、自动剔除故障节点、重试机制。
|
||||||
|
- **无感透传**: 自动处理微服务间的 Header 透传(如 TraceID、UserID 等)。
|
||||||
|
- **多协议支持**: 支持 HTTP/1.1、HTTP/2 (H2C)、WebSocket。
|
||||||
|
|
||||||
## 🎯 设计哲学
|
## 配置参考
|
||||||
|
```yaml
|
||||||
- **纯粹无状态 (Stateless)**:模块自身不读取配置文件,不依赖任何特定框架的上下文。配置加载由调用方负责,参数通过入口函数注入。
|
discover:
|
||||||
- **面向对象隔离**:支持多实例共存。可以在同一个进程中同时连接不同的注册中心,实现复杂的网关分发。
|
registry: redis://127.0.0.1:6379/15 # 注册中心地址
|
||||||
- **内存安全与高性能**:访问令牌 (Token) 强制受 `@go/safe` 内存保护;调用耗时由 `@go/timer` 追踪;网络层支持 H2C (HTTP/2 Cleartext)。
|
app: my-service # 当前应用名称
|
||||||
|
weight: 100 # 节点权重
|
||||||
## 📦 安装
|
calls: # 调用的服务定义
|
||||||
|
auth: 1s:my-token:2 # 服务名: 超时:Token:HTTP版本
|
||||||
```bash
|
user: 500ms
|
||||||
go get apigo.cc/go/discover
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## API 指南
|
||||||
|
|
||||||
## 🛠 API Reference
|
### 初始化与启动
|
||||||
|
- `Start(addr string) bool`: 启动服务发现,指定当前节点的外部访问地址。
|
||||||
|
- `EasyStart() (string, int)`: 自动监听可用端口并启动服务发现。返回 IP 和端口。
|
||||||
|
- `Stop()`: 停止服务并注销节点。
|
||||||
|
|
||||||
### 1. 核心构造函数 (Entry Points)
|
### 服务调用 (Caller)
|
||||||
|
- `NewCaller(request *http.Request, logger *log.Logger) *Caller`: 创建调用器。传入原始请求可自动透传 Header。
|
||||||
|
- `Caller.Get / Post / Put / Delete / Head`: 发起同步请求。
|
||||||
|
- `Caller.Do(method, app, path, data, headers...)`: 发起通用请求,返回 `http.Result`。
|
||||||
|
- `Caller.Open(app, path, headers...)`: 发起 WebSocket 连接。
|
||||||
|
|
||||||
#### Start: 服务端模式
|
### 手动管理
|
||||||
在注册中心登记当前节点。
|
- `AddExternalApp(app, callConf string)`: 手动添加需要发现的外部应用。
|
||||||
- **原型**: `func Start(registry, app, addr string, logger *log.Logger, confs ...Config) *Discoverer`
|
- `SetNode(app, addr string, weight int)`: 手动设置某个服务的节点信息。
|
||||||
- **参数**:
|
|
||||||
- `registry`: 注册中心地址。支持 Redis URL (如 `redis://127.0.0.1:6379/15`) 或 `@go/redis` 下定义的 Redis 配置键名。
|
|
||||||
- `app`: 当前应用名称。
|
|
||||||
- `addr`: 当前节点外部可访问的地址 (如 `192.168.1.10:8080`)。
|
|
||||||
- `logger`: 必填。建议传入带有 TraceID 的 Logger 以确保链路可追踪。允许传 `nil` (回退至 `log.DefaultLogger`)。
|
|
||||||
- `confs`: 可选。传递 `discover.Config` 结构体进行精细化配置。
|
|
||||||
|
|
||||||
#### Open: 纯客户端模式
|
### 负载均衡与路由
|
||||||
仅用于调用其他服务。
|
- `SetLoadBalancer(lb LoadBalancer)`: 自定义全局负载均衡策略。
|
||||||
- **原型**: `func Open(registry string, logger *log.Logger, confs ...Config) *Discoverer`
|
- `SetRoute(route func(ac *AppClient, r *http.Request))`: 设置全局路由拦截规则。
|
||||||
|
|
||||||
### 2. Discoverer 实例方法 (RPC 调用)
|
## 环境变量
|
||||||
|
- `DISCOVER_REGISTRY`: 注册中心地址。
|
||||||
所有的业务调用均应通过 `Start` 或 `Open` 返回的 `*Discoverer` 实例进行。
|
- `DISCOVER_APP`: 应用名。
|
||||||
|
- `DISCOVER_WEIGHT`: 节点权重。
|
||||||
#### 基础 HTTP 调用
|
- `DISCOVER_CALLS`: 调用的应用定义。
|
||||||
返回 `*gohttp.Result`,可结合 `go/http.To[T]` 实现结果绑定。
|
- `DISCOVER_LISTEN`: EasyStart 监听地址。
|
||||||
- `func (d *Discoverer) Get(app, path string, headers ...string) *gohttp.Result`
|
|
||||||
- `func (d *Discoverer) Post(app, path string, data any, headers ...string) *gohttp.Result`
|
|
||||||
- `func (d *Discoverer) Put(app, path string, data any, headers ...string) *gohttp.Result`
|
|
||||||
- `func (d *Discoverer) Delete(app, path string, data any, headers ...string) *gohttp.Result`
|
|
||||||
- `func (d *Discoverer) Head(app, path string, headers ...string) *gohttp.Result`
|
|
||||||
- `func (d *Discoverer) Do(method, app, path string, data any, headers ...string) *gohttp.Result`
|
|
||||||
|
|
||||||
#### 链路透传调用 (Context Propagation)
|
|
||||||
通过 `From(r)` 提取原始请求上下文(TraceID, UserID 等)并向后透传。
|
|
||||||
- **原型**: `func (d *Discoverer) From(request *http.Request) *Caller`
|
|
||||||
- **示例**: `res := d.From(r).Post("user-service", "/create", reqData)`
|
|
||||||
|
|
||||||
#### WebSocket 支持
|
|
||||||
- **原型**: `func (d *Discoverer) Open(app, path string, headers ...string) *websocket.Conn`
|
|
||||||
|
|
||||||
#### 实例生命周期
|
|
||||||
- `func (d *Discoverer) Stop()`: 优雅停止心跳、注销节点并释放内部连接池。
|
|
||||||
|
|
||||||
### 3. 配置结构 (Strongly Typed Config)
|
|
||||||
|
|
||||||
#### Config: 发现器配置
|
|
||||||
```go
|
|
||||||
type Config struct {
|
|
||||||
Weight int // 节点权重 (默认 100)
|
|
||||||
Calls map[string]CallConfig // 依赖服务的调用配置
|
|
||||||
CallRetryTimes int // 下游节点的最大重试次数 (默认 10)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### CallConfig: 下游服务调用配置
|
|
||||||
```go
|
|
||||||
type CallConfig struct {
|
|
||||||
Timeout time.Duration // 超时时间
|
|
||||||
Token *safe.SafeBuf // 访问凭据 (强制安全存储,防止内存泄露)
|
|
||||||
Http2 bool // 是否强制使用 HTTP/2 (H2C)
|
|
||||||
SSL bool // 是否使用 HTTPS/WSS 协议
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 最佳实践示例
|
|
||||||
|
|
||||||
### 标准服务端启动
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"apigo.cc/go/discover"
|
|
||||||
"apigo.cc/go/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 准备安全令牌
|
|
||||||
token := safe.NewSafeBuf([]byte("secure-app-token"))
|
|
||||||
|
|
||||||
d := discover.Start(
|
|
||||||
"redis://127.0.0.1:6379/15",
|
|
||||||
"user-service",
|
|
||||||
"192.168.1.10:8080",
|
|
||||||
logger,
|
|
||||||
discover.Config{
|
|
||||||
Calls: map[string]discover.CallConfig{
|
|
||||||
"auth-service": { Timeout: time.Second, Token: token },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
defer d.Stop()
|
|
||||||
|
|
||||||
// 调用并自动解析
|
|
||||||
res := d.Get("auth-service", "/api/verify")
|
|
||||||
user, err := http.To[User](res)
|
|
||||||
```
|
|
||||||
|
|||||||
6
Route.go
6
Route.go
@ -2,7 +2,7 @@ package discover
|
|||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
// SetRoute 设置路由规则
|
// SetRoute 设置全局路由规则,可以在请求前修改 App、Method、Path 等信息
|
||||||
func (d *Discoverer) SetRoute(route func(appClient *AppClient, request *http.Request)) {
|
func SetRoute(route func(appClient *AppClient, request *http.Request)) {
|
||||||
d.settedRoute = route
|
settedRoute = route
|
||||||
}
|
}
|
||||||
|
|||||||
28
TEST.md
28
TEST.md
@ -1,19 +1,15 @@
|
|||||||
# Discover Module Test Report
|
# Test Report
|
||||||
|
|
||||||
## Test Coverage
|
## 测试场景
|
||||||
- **Standard Discovery**: Basic registration and discovery via Redis. (Verified in `TestDiscover`)
|
1. **基础发现与调用**: 验证服务启动后能自动注册到 Redis,并能通过 Caller 正确发起请求。
|
||||||
- **WebSocket Support**: Caller supports transparent WebSocket proxying. (Verified in `TestDiscover`)
|
2. **实时同步**: 验证通过 Redis PUBLISH 更新节点信息后,客户端能实时感知并更新本地节点列表。
|
||||||
- **Multi-Instance Isolation**: Verified that multiple `Discoverer` instances can coexist and correctly discover each other when configured, while remaining isolated in their own lifecycle. (Verified in `TestMultipleDiscoverer`)
|
3. **故障剔除**: 验证当节点调用持续失败时,能自动从本地列表中剔除。
|
||||||
- **EasyStart**: Automatic IP and port detection for zero-config startup. (Verified in `TestEasyStart`)
|
4. **环境变量配置**: 验证 `EasyStart` 结合环境变量的启动流程。
|
||||||
|
|
||||||
## Deadlock & Stability Fixes
|
## 测试结果
|
||||||
- **Stop() Deadlock**: Fixed by releasing `appLock` before calling blocking Redis/PubSub operations.
|
- **Unit Tests**: `go test -v ./...`
|
||||||
- **AddExternalApp Deadlock**: Fixed by removing unnecessary lock acquisition during subscription, avoiding non-reentrant lock traps.
|
- `TestDiscover`: PASS
|
||||||
- **Daemon Shutdown**: Improved daemon thread exit logic using `select` and `close(signal)`, ensuring immediate termination upon `Stop()`.
|
- `TestEasyStart`: PASS
|
||||||
- **H2C Optimization**: Avoided slow retries in tests by explicitly configuring HTTP/1.1 for mock servers.
|
|
||||||
|
|
||||||
## Benchmarks
|
## Benchmark
|
||||||
- **BenchmarkDiscover**: ~500 ns/op (Core node selection and load balancing logic)
|
- 待补充(Discover 主要性能开销在负载均衡算法选择,单次选择耗时极低)。
|
||||||
|
|
||||||
> Date: 2026-05-05
|
|
||||||
> Environment: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
package discover_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/discover"
|
|
||||||
gohttp "apigo.cc/go/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TestResult struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestElegantAPI(t *testing.T) {
|
|
||||||
// 1. 模拟服务
|
|
||||||
l, _ := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
addr := l.Addr().String()
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, _ = w.Write([]byte(`{"message":"ok"}`))
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, _ = w.Write([]byte(`{"message":"posted"}`))
|
|
||||||
})
|
|
||||||
server := &http.Server{Handler: mux}
|
|
||||||
go func() { _ = server.Serve(l) }()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
// 2. 配置并启动 Discover
|
|
||||||
d := discover.Start("redis://127.0.0.1:6379/15", "api-test", addr, nil)
|
|
||||||
if d == nil {
|
|
||||||
t.Skip("redis not available")
|
|
||||||
}
|
|
||||||
defer d.Stop()
|
|
||||||
|
|
||||||
// 添加外部应用调用配置
|
|
||||||
d.AddExternalApp("api-test", discover.CallConfig{})
|
|
||||||
|
|
||||||
// 等待节点同步
|
|
||||||
for i := 0; i < 20; i++ {
|
|
||||||
if nodes := d.GetAppNodes("api-test"); len(nodes) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 测试调用并解析 (Stateless)
|
|
||||||
res, err := gohttp.To[TestResult](d.Get("api-test", "/get"))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Get failed: %v", err)
|
|
||||||
}
|
|
||||||
if res.Message != "ok" {
|
|
||||||
t.Errorf("unexpected message: %s", res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
res2, err := gohttp.To[TestResult](d.Post("api-test", "/post", map[string]string{"foo": "bar"}))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Post failed: %v", err)
|
|
||||||
}
|
|
||||||
if res2.Message != "posted" {
|
|
||||||
t.Errorf("unexpected message: %s", res2.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 测试透传调用 (Stateful)
|
|
||||||
req, _ := http.NewRequest("GET", "http://example.com", nil)
|
|
||||||
req.Header.Set("X-Request-ID", "req-123")
|
|
||||||
|
|
||||||
rawRes3 := d.From(req).Get("api-test", "/get")
|
|
||||||
res3, err := gohttp.To[TestResult](rawRes3)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("From(r).Get failed: %v", err)
|
|
||||||
}
|
|
||||||
if res3.Message != "ok" {
|
|
||||||
t.Errorf("unexpected message: %s", res3.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 测试直接获取 Result
|
|
||||||
rawRes := d.Do("GET", "api-test", "/get", nil)
|
|
||||||
if rawRes.Error != nil {
|
|
||||||
t.Errorf("Do failed: %v", rawRes.Error)
|
|
||||||
}
|
|
||||||
if rawRes.String() != `{"message":"ok"}` {
|
|
||||||
t.Errorf("unexpected raw string: %s", rawRes.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
go.mod
33
go.mod
@ -3,26 +3,27 @@ module apigo.cc/go/discover
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.2.8
|
apigo.cc/go/cast v1.2.6
|
||||||
apigo.cc/go/config v1.0.7
|
apigo.cc/go/config v1.0.4
|
||||||
apigo.cc/go/http v1.0.10
|
apigo.cc/go/http v1.0.3
|
||||||
apigo.cc/go/id v1.0.5
|
apigo.cc/go/id v1.0.4
|
||||||
apigo.cc/go/log v1.1.13
|
apigo.cc/go/log v1.0.2
|
||||||
apigo.cc/go/redis v1.0.7
|
apigo.cc/go/redis v1.0.2
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/crypto v1.1.0 // indirect
|
apigo.cc/go/convert v1.0.4 // indirect
|
||||||
apigo.cc/go/encoding v1.1.1 // indirect
|
apigo.cc/go/crypto v1.0.4 // indirect
|
||||||
apigo.cc/go/file v1.0.7 // indirect
|
apigo.cc/go/encoding v1.0.4 // indirect
|
||||||
apigo.cc/go/rand v1.0.5 // indirect
|
apigo.cc/go/file v1.0.4 // indirect
|
||||||
apigo.cc/go/safe v1.0.5 // indirect
|
apigo.cc/go/rand v1.0.4 // indirect
|
||||||
apigo.cc/go/shell v1.0.5 // indirect
|
apigo.cc/go/safe v1.0.4 // indirect
|
||||||
|
apigo.cc/go/shell v1.0.4 // indirect
|
||||||
github.com/gomodule/redigo v1.9.3 // indirect
|
github.com/gomodule/redigo v1.9.3 // indirect
|
||||||
golang.org/x/crypto v0.51.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/net v0.54.0 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
71
go.sum
71
go.sum
@ -1,61 +1,48 @@
|
|||||||
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI=
|
apigo.cc/go/cast v1.2.6 h1:xnWiaQAGsRCrnu1p8fIFQfg5HFSc7CxR+3ItiDIDMaY=
|
||||||
apigo.cc/go/cast v1.2.8/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
apigo.cc/go/cast v1.2.6/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||||
apigo.cc/go/config v1.0.7 h1:lldkjsuUrWVHz/0y08/pF1vbsTvZC3TNQ2tFQ38jRNw=
|
apigo.cc/go/config v1.0.4 h1:WG9zrQkqfFPkrKIL7RNvvAbbkuUBt1Av11ZP/aIfldM=
|
||||||
apigo.cc/go/config v1.0.7/go.mod h1:9oogTK83NvNmvAEIe/zuK2EKOnDtNz3bZoVqvyhFMW8=
|
apigo.cc/go/config v1.0.4/go.mod h1:obryzJiK6j7lQex/58d5eWYOGx5O5IABguqNWxyyXJo=
|
||||||
apigo.cc/go/crypto v1.1.0 h1:dv9ZRbtJHnnLbDHUfjP//GHLniu0/5ja0w5QE5hwwOU=
|
apigo.cc/go/convert v1.0.4 h1:5+qPjC3dlPB59GnWZRlmthxcaXQtKvN+iOuiLdJ1GvQ=
|
||||||
apigo.cc/go/crypto v1.1.0/go.mod h1:0NUsQMGiP95TWHJexb3F1MxNdW+LR8TD1VqwHPN8PR8=
|
apigo.cc/go/convert v1.0.4/go.mod h1:Hp+geeSyhqg/zwIKPOrDoceIREzcwM14t1I5q/dtbfU=
|
||||||
apigo.cc/go/encoding v1.1.1 h1:p/57IfKIeB+b3rTfZSN3KegZigfPOEEfuuuOmZuc+sM=
|
apigo.cc/go/crypto v1.0.4 h1:VPUyHCH2N3LLEgdpwUc+DQssNHzLlxVzLNRa0Jm6O4o=
|
||||||
apigo.cc/go/encoding v1.1.1/go.mod h1:GeAz5OnCkFybTR1+GWFqdMgfq5v6r4MsjWVPOk/mpf4=
|
apigo.cc/go/crypto v1.0.4/go.mod h1:5sI8BLw6YHZfDReYwCO3TFD2LKm36HMdLg1S5oPv/QU=
|
||||||
apigo.cc/go/file v1.0.7 h1:j1VBtmMZqNGnH++DYjHecX1XAKTlKAuqUiUW1HafRas=
|
apigo.cc/go/encoding v1.0.4 h1:aezB0J/qFuHs6iXkbtuJP5JIHUtmjsr5SFb0NNvbObY=
|
||||||
apigo.cc/go/file v1.0.7/go.mod h1:2qC+p8p7iHx0DHAPubHXkLrEuLGO9WXTtdwyFjrSc1I=
|
apigo.cc/go/encoding v1.0.4/go.mod h1:V5CgT7rBbCxy+uCU20q0ptcNNRSgMtpA8cNOs6r8IeI=
|
||||||
apigo.cc/go/http v1.0.9 h1:fDJ11Rj/GhigSR0ROSHm0oWJidBCZ56+B+ntoEtw6+g=
|
apigo.cc/go/file v1.0.4 h1:qCKegV7OYh7r0qc3jZjGA/aKh0vIHgmr1OEbhfEmGX8=
|
||||||
apigo.cc/go/http v1.0.9/go.mod h1:ca9LYy3TURMUKKB5654ofpRgNqLaq5ibxOrWb5PgOiU=
|
apigo.cc/go/file v1.0.4/go.mod h1:C9gNo7386iA21OiBmuWh6CznKWlVBDFkhE4f0H0Susg=
|
||||||
apigo.cc/go/id v1.0.5 h1:23YkR7oklSA69gthYlu8zl/kpIkeIoEYxi1f1Sz5l3A=
|
apigo.cc/go/http v1.0.3 h1:c19ppdb7gR9aIPeY3qOjOj4X3+jZLXln76jTTj7i4vM=
|
||||||
apigo.cc/go/id v1.0.5/go.mod h1:ZaYLIyrJvkf3j7J8a0lnKywSAHljaczWxU0x2HmQDzg=
|
apigo.cc/go/http v1.0.3/go.mod h1:oHQYlBLN6u53C2t1BihxT7cnUQd+zLTAYr3ALjWUkpg=
|
||||||
apigo.cc/go/log v1.1.13 h1:ZABeVA9DxhdneLqHrYEc+6YijgoygG8eEsgDxYDzpDc=
|
apigo.cc/go/id v1.0.4 h1:w+JSdeVit52iefIUolrh1qLEZS9XqHNKr1UygFcgv+s=
|
||||||
apigo.cc/go/log v1.1.13/go.mod h1:eabuI2SynGNgo5FXPbGgQtyxjp94wT643XzjYhEIP3A=
|
apigo.cc/go/id v1.0.4/go.mod h1:kg7QuceAKtGNzGWt0+pIIh8Qom1eMSWGb8+0Yhi/QVY=
|
||||||
apigo.cc/go/rand v1.0.5 h1:AkUoWr0SELgeDmRjLEDjOIp29nXdzqQQvmGRIHpTN7U=
|
apigo.cc/go/log v1.0.2 h1:OY6T3SC28blDNkMpdRvDK2N4sGdriAB9DBItGl/qOos=
|
||||||
apigo.cc/go/rand v1.0.5/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
apigo.cc/go/log v1.0.2/go.mod h1:tvPgFpebY9Wf/DlqMHZ0ZjxDp9AaQTywOQKvtBaNqNo=
|
||||||
apigo.cc/go/redis v1.0.7 h1:WFhbsjwIdUsmBcpMO45QTHlKu/nETpcRewLWqF5TnpQ=
|
apigo.cc/go/rand v1.0.4 h1:we070eWSL0dB8NEMaWjXj43+EekXQTm/h0kKpZ/frqw=
|
||||||
apigo.cc/go/redis v1.0.7/go.mod h1:gEgnzhrrlZHL6XzsKEG+zR2y6l/eWIbwdT1dbhbG/7g=
|
apigo.cc/go/rand v1.0.4/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||||
apigo.cc/go/safe v1.0.5 h1:yZJLhpMntJrtqU/ev0UlyOoHu/cLrnnGUO4aHyIZcwE=
|
apigo.cc/go/redis v1.0.2 h1:gWBrL/6eDxtouTFSZrPKQNdEg1AZr2aKTpCOhwim3dI=
|
||||||
apigo.cc/go/safe v1.0.5/go.mod h1:i9xnh7reJIFPauLnlzuIDgvrQvhjxpFlpVh3O6ulWd0=
|
apigo.cc/go/redis v1.0.2/go.mod h1:auQ3cyORgD67HF5dNvZ1lA8bqMH1xIbnuKBuZWclNy4=
|
||||||
apigo.cc/go/shell v1.0.5 h1:bmvUTJGe1GwsHAy42v3iaoK40PoBC7Xq1aMCYxUZmtg=
|
apigo.cc/go/safe v1.0.4 h1:07pRSdEHprF/2v6SsqAjICYFoeLcqjjvHGEdh6Dzrzg=
|
||||||
apigo.cc/go/shell v1.0.5/go.mod h1:sx/nYw5CihHWmo5JHkaZUbmMYXNHx8swzArbQCUGHjc=
|
apigo.cc/go/safe v1.0.4/go.mod h1:o568sHS5rTRSVPmhxWod0tGdc+8l1KjidsNY1/OVZr0=
|
||||||
|
apigo.cc/go/shell v1.0.4 h1:EL9zjI39YBe1h+kRYQeAi/8zVGHe5W198DYYN7cENiY=
|
||||||
|
apigo.cc/go/shell v1.0.4/go.mod h1:N2gDkgK4tJ9TadD60/+gAGuWxyVAWHs5YPBmytw6ELA=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
|
||||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user