完善通道管理机制 & 增强 otel 记录字段
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- 选择代理部分,可以检查 redis 中的可用端口数量,无需查数据库
|
- 解决提取成功率问题
|
||||||
- 取用端口后直接写入关闭任务,避免中途失败导致端口泄漏
|
- otel 没有正确记录接口失败信息
|
||||||
-
|
- 购买数低于最小限制
|
||||||
|
|
||||||
---
|
创建 channel 时以 defer 的思路执行 fallback 函数,确保执行幂等性
|
||||||
|
|
||||||
错误提示增强,展示整链路信息
|
错误提示增强,展示整链路信息
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,11 @@ const (
|
|||||||
ScopeBatchReadOfUser = string("batch:read:of_user") // 读取指定用户的批次列表
|
ScopeBatchReadOfUser = string("batch:read:of_user") // 读取指定用户的批次列表
|
||||||
ScopeBatchWrite = string("batch:write") // 写入批次
|
ScopeBatchWrite = string("batch:write") // 写入批次
|
||||||
|
|
||||||
ScopeChannel = string("channel") // IP
|
ScopeChannel = string("channel") // IP
|
||||||
ScopeChannelRead = string("channel:read") // 读取 IP 列表
|
ScopeChannelRead = string("channel:read") // 读取 IP 列表
|
||||||
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表
|
ScopeChannelReadOfUser = string("channel:read:of_user") // 读取指定用户的 IP 列表
|
||||||
ScopeChannelWrite = string("channel:write") // 写入 IP
|
ScopeChannelWrite = string("channel:write") // 写入 IP
|
||||||
|
ScopeChannelWriteClearExpired = string("channel:write:clear_expired") // 清理过期 IP
|
||||||
|
|
||||||
ScopeProxy = string("proxy") // 代理
|
ScopeProxy = string("proxy") // 代理
|
||||||
ScopeProxyRead = string("proxy:read") // 读取代理列表
|
ScopeProxyRead = string("proxy:read") // 读取代理列表
|
||||||
|
|||||||
@@ -9,9 +9,3 @@ const RemoveChannel = "channel:remove"
|
|||||||
func NewRemoveChannel(batch string) *asynq.Task {
|
func NewRemoveChannel(batch string) *asynq.Task {
|
||||||
return asynq.NewTask(RemoveChannel, []byte(batch))
|
return asynq.NewTask(RemoveChannel, []byte(batch))
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClearExpiredChannels = "channel:clear_expired"
|
|
||||||
|
|
||||||
func NewClearExpiredChannels() *asynq.Task {
|
|
||||||
return asynq.NewTask(ClearExpiredChannels, nil)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ func RemoveChannels(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除通道
|
// 删除通道
|
||||||
err = s.Channel.RemoveChannels(req.Batch)
|
err = s.Channel.RemoveChannels(req.Batch, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -341,3 +341,32 @@ type PageChannelOfUserByAdminReq struct {
|
|||||||
ExpiredAtStart *time.Time `json:"expired_at_start"`
|
ExpiredAtStart *time.Time `json:"expired_at_start"`
|
||||||
ExpiredAtEnd *time.Time `json:"expired_at_end"`
|
ExpiredAtEnd *time.Time `json:"expired_at_end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SyncChannelClearExpiredByAdmin 清理过期通道
|
||||||
|
func SyncChannelClearExpiredByAdmin(c *fiber.Ctx) error {
|
||||||
|
if _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeChannelWriteClearExpired); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SyncChannelClearExpiredByAdminReq
|
||||||
|
if err := g.Validator.ParseBody(c, &req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := s.Channel.ClearExpiredChannels(req.ProxyID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(SyncChannelClearExpiredByAdminResp{
|
||||||
|
Count: count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncChannelClearExpiredByAdminReq struct {
|
||||||
|
ProxyID int32 `json:"proxy_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncChannelClearExpiredByAdminResp struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ func RemoveProxy(c *fiber.Ctx) error {
|
|||||||
return c.JSON(nil)
|
return c.JSON(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================
|
||||||
|
|
||||||
// region 报告上线
|
// region 报告上线
|
||||||
func ProxyReportOnline(c *fiber.Ctx) (err error) {
|
func ProxyReportOnline(c *fiber.Ctx) (err error) {
|
||||||
return c.JSON(map[string]any{
|
return c.JSON(map[string]any{
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jxskiss/base62"
|
"github.com/jxskiss/base62"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ApplyMiddlewares(app *fiber.App) {
|
func ApplyMiddlewares(app *fiber.App) {
|
||||||
@@ -20,14 +22,6 @@ func ApplyMiddlewares(app *fiber.App) {
|
|||||||
EnableStackTrace: true,
|
EnableStackTrace: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// cors
|
|
||||||
app.Use(cors.New(cors.Config{
|
|
||||||
AllowCredentials: true,
|
|
||||||
AllowOriginsFunc: func(origin string) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// logger
|
// logger
|
||||||
app.Use(logger.New(logger.Config{
|
app.Use(logger.New(logger.Config{
|
||||||
Next: func(c *fiber.Ctx) bool {
|
Next: func(c *fiber.Ctx) bool {
|
||||||
@@ -37,6 +31,34 @@ func ApplyMiddlewares(app *fiber.App) {
|
|||||||
|
|
||||||
// metric
|
// metric
|
||||||
app.Use(otelfiber.Middleware())
|
app.Use(otelfiber.Middleware())
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
err := c.Next()
|
||||||
|
|
||||||
|
span := trace.SpanFromContext(c.UserContext())
|
||||||
|
if !span.IsRecording() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := c.Response().StatusCode()
|
||||||
|
body := []byte{}
|
||||||
|
if status < 200 || status >= 300 {
|
||||||
|
body = c.Response().Body()
|
||||||
|
if len(body) > 1024 {
|
||||||
|
body = body[:1024]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.SetAttributes(attribute.String("http.response.error", string(body)))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
// cors
|
||||||
|
app.Use(cors.New(cors.Config{
|
||||||
|
AllowCredentials: true,
|
||||||
|
AllowOriginsFunc: func(origin string) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
// request id
|
// request id
|
||||||
app.Use(requestid.New(requestid.Config{
|
app.Use(requestid.New(requestid.Config{
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package web
|
|||||||
import (
|
import (
|
||||||
"platform/pkg/env"
|
"platform/pkg/env"
|
||||||
auth2 "platform/web/auth"
|
auth2 "platform/web/auth"
|
||||||
|
"platform/web/core"
|
||||||
"platform/web/handlers"
|
"platform/web/handlers"
|
||||||
s "platform/web/services"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
@@ -34,11 +34,8 @@ func ApplyRouters(app *fiber.App) {
|
|||||||
}
|
}
|
||||||
return ctx.JSON(rs)
|
return ctx.JSON(rs)
|
||||||
})
|
})
|
||||||
debug.Get("/channel/clear-expired", func(ctx *fiber.Ctx) error {
|
debug.Get("/test/err", func(ctx *fiber.Ctx) error {
|
||||||
if err := s.Channel.ClearExpiredChannels(); err != nil {
|
return core.NewBizErr("测试错误")
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ctx.SendStatus(fiber.StatusOK)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +205,7 @@ func adminRouter(api fiber.Router) {
|
|||||||
var channel = api.Group("/channel")
|
var channel = api.Group("/channel")
|
||||||
channel.Post("/page", handlers.PageChannelByAdmin)
|
channel.Post("/page", handlers.PageChannelByAdmin)
|
||||||
channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin)
|
channel.Post("/page/of-user", handlers.PageChannelOfUserByAdmin)
|
||||||
|
channel.Post("/sync/clear-expired", handlers.SyncChannelClearExpiredByAdmin)
|
||||||
|
|
||||||
// proxy 代理
|
// proxy 代理
|
||||||
var proxy = api.Group("/proxy")
|
var proxy = api.Group("/proxy")
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ var Channel = &channelServer{
|
|||||||
|
|
||||||
type ChannelServiceProvider interface {
|
type ChannelServiceProvider interface {
|
||||||
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
|
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter *EdgeFilter) ([]*m.Channel, error)
|
||||||
RemoveChannels(batch string) error
|
RemoveChannels(batch string, proxyId *int32) error
|
||||||
ClearExpiredChannels() error
|
ClearExpiredChannels(proxyId int32) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type channelServer struct {
|
type channelServer struct {
|
||||||
@@ -37,12 +37,12 @@ func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, auth
|
|||||||
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter)
|
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelServer) RemoveChannels(batch string) error {
|
func (s *channelServer) RemoveChannels(batch string, proxyId *int32) error {
|
||||||
return s.provider.RemoveChannels(batch)
|
return s.provider.RemoveChannels(batch, proxyId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelServer) ClearExpiredChannels() error {
|
func (s *channelServer) ClearExpiredChannels(proxyId int32) (int, error) {
|
||||||
return s.provider.ClearExpiredChannels()
|
return s.provider.ClearExpiredChannels(proxyId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 授权方式
|
// 授权方式
|
||||||
@@ -220,10 +220,13 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, authWhitelist bo
|
|||||||
return resource, ips, nil
|
return resource, ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func freeChansKey(proxy int32) string {
|
||||||
freeChansKey = "channel:free"
|
return "channel:free:" + strconv.Itoa(int(proxy))
|
||||||
usedChansKey = "channel:used"
|
}
|
||||||
)
|
|
||||||
|
func usedChansKey(proxy int32, batch string) string {
|
||||||
|
return "channel:used:" + strconv.Itoa(int(proxy)) + ":" + batch
|
||||||
|
}
|
||||||
|
|
||||||
// 扩容通道
|
// 扩容通道
|
||||||
func regChans(proxy int32, chans []netip.AddrPort) error {
|
func regChans(proxy int32, chans []netip.AddrPort) error {
|
||||||
@@ -232,7 +235,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
|
|||||||
strs[i] = ch.String()
|
strs[i] = ch.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
|
key := freeChansKey(proxy)
|
||||||
err := g.Redis.SAdd(context.Background(), key, strs...).Err()
|
err := g.Redis.SAdd(context.Background(), key, strs...).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("扩容通道失败: %w", err)
|
return fmt.Errorf("扩容通道失败: %w", err)
|
||||||
@@ -242,7 +245,7 @@ func regChans(proxy int32, chans []netip.AddrPort) error {
|
|||||||
|
|
||||||
// 缩容通道
|
// 缩容通道
|
||||||
func remChans(proxy int32) error {
|
func remChans(proxy int32) error {
|
||||||
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
|
key := freeChansKey(proxy)
|
||||||
err := g.Redis.Del(context.Background(), key).Err()
|
err := g.Redis.Del(context.Background(), key).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("缩容通道失败: %w", err)
|
return fmt.Errorf("缩容通道失败: %w", err)
|
||||||
@@ -252,13 +255,12 @@ func remChans(proxy int32) error {
|
|||||||
|
|
||||||
// 取用通道
|
// 取用通道
|
||||||
func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
|
func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
|
||||||
pid := strconv.Itoa(int(proxy))
|
|
||||||
chans, err := RedisScriptLockChans.Run(
|
chans, err := RedisScriptLockChans.Run(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
g.Redis,
|
g.Redis,
|
||||||
[]string{
|
[]string{
|
||||||
freeChansKey + ":" + pid,
|
freeChansKey(proxy),
|
||||||
usedChansKey + ":" + pid + ":" + batch,
|
usedChansKey(proxy, batch),
|
||||||
},
|
},
|
||||||
count,
|
count,
|
||||||
).StringSlice()
|
).StringSlice()
|
||||||
@@ -296,13 +298,12 @@ return ports
|
|||||||
|
|
||||||
// 归还通道
|
// 归还通道
|
||||||
func freeChans(proxy int32, batch string) error {
|
func freeChans(proxy int32, batch string) error {
|
||||||
pid := strconv.Itoa(int(proxy))
|
|
||||||
err := RedisScriptFreeChans.Run(
|
err := RedisScriptFreeChans.Run(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
g.Redis,
|
g.Redis,
|
||||||
[]string{
|
[]string{
|
||||||
freeChansKey + ":" + pid,
|
freeChansKey(proxy),
|
||||||
usedChansKey + ":" + pid + ":" + batch,
|
usedChansKey(proxy, batch),
|
||||||
},
|
},
|
||||||
).Err()
|
).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -39,6 +40,15 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
|||||||
user := resource.User
|
user := resource.User
|
||||||
expire := now.Add(resource.Live)
|
expire := now.Add(resource.Live)
|
||||||
|
|
||||||
|
// 注册异步关闭任务
|
||||||
|
_, err = g.Asynq.Enqueue(
|
||||||
|
e.NewRemoveChannel(batch),
|
||||||
|
asynq.ProcessAt(expire),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("注册异步关闭通道任务失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 选择代理
|
// 选择代理
|
||||||
proxyResult := struct {
|
proxyResult := struct {
|
||||||
m.Proxy
|
m.Proxy
|
||||||
@@ -85,14 +95,6 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = g.Asynq.Enqueue(
|
|
||||||
e.NewRemoveChannel(batch),
|
|
||||||
asynq.ProcessAt(expire),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, core.NewServErr("提交关闭通道任务失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取用节点
|
// 取用节点
|
||||||
secret := strings.Split(u.Z(proxy.Secret), ":")
|
secret := strings.Split(u.Z(proxy.Secret), ":")
|
||||||
if len(secret) != 2 {
|
if len(secret) != 2 {
|
||||||
@@ -156,6 +158,32 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提交配置
|
||||||
|
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "total_count", len(chanConfigs), "remote_count", len(edgeConfigs))
|
||||||
|
if env.RunMode == env.RunModeProd {
|
||||||
|
|
||||||
|
// 连接节点到网关
|
||||||
|
err = g.Cloud.CloudConnect(&g.CloudConnectReq{
|
||||||
|
Uuid: proxy.Mac,
|
||||||
|
Edge: &edgeConfigs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("连接云平台失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用网关代理通道
|
||||||
|
err = gateway.GatewayPortConfigs(chanConfigs)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("提交代理端口配置失败", "error", err.Error())
|
||||||
|
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, item := range chanConfigs {
|
||||||
|
str, _ := json.Marshal(item)
|
||||||
|
fmt.Println(string(str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保存数据
|
// 保存数据
|
||||||
err = q.Q.Transaction(func(q *q.Query) error {
|
err = q.Q.Transaction(func(q *q.Query) error {
|
||||||
// 更新使用记录
|
// 更新使用记录
|
||||||
@@ -224,124 +252,155 @@ func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交配置
|
|
||||||
if env.RunMode == env.RunModeProd {
|
|
||||||
|
|
||||||
// 连接节点到网关
|
|
||||||
err = g.Cloud.CloudConnect(&g.CloudConnectReq{
|
|
||||||
Uuid: proxy.Mac,
|
|
||||||
Edge: &edgeConfigs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, core.NewServErr("连接云平台失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启用网关代理通道
|
|
||||||
err = gateway.GatewayPortConfigs(chanConfigs)
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("提交代理端口配置失败", "error", err.Error())
|
|
||||||
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "count", len(chanConfigs), "remote", len(edgeConfigs))
|
|
||||||
for _, item := range chanConfigs {
|
|
||||||
str, _ := json.Marshal(item)
|
|
||||||
fmt.Println(string(str))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels, nil
|
return channels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
|
func (s *channelBaiyinProvider) RemoveChannels(batch string, proxyId *int32) error {
|
||||||
start := time.Now()
|
return g.Redsync.WithLock(batchRemoveExpiredKey(batch), func() error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
// 获取连接数据
|
pid := int32(0)
|
||||||
channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find()
|
if proxyId == nil {
|
||||||
if err != nil {
|
// 获取连接数据
|
||||||
return core.NewServErr(fmt.Sprintf("获取通道数据失败,batch:%s", batch), err)
|
channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find()
|
||||||
}
|
if err != nil {
|
||||||
if len(channels) == 0 {
|
return core.NewServErr(fmt.Sprintf("获取通道数据失败,batch:%s", batch), err)
|
||||||
slog.Warn(fmt.Sprintf("未找到通道数据,batch:%s", batch))
|
}
|
||||||
return nil
|
if len(channels) == 0 {
|
||||||
}
|
slog.Warn(fmt.Sprintf("未找到通道数据,batch:%s", batch))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr(fmt.Sprintf("获取代理数据失败,batch:%s", batch), err)
|
return core.NewServErr(fmt.Sprintf("获取代理数据失败,batch:%s", batch), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备配置数据
|
// 检查通道是否存在
|
||||||
edgeConfigs := make([]string, len(channels))
|
exist, err := g.Redis.Exists(context.Background(), usedChansKey(proxy.ID, batch)).Result()
|
||||||
configs := make([]*g.PortConfigsReq, len(channels))
|
if err != nil {
|
||||||
for i, channel := range channels {
|
return core.NewServErr("查询使用中通道失败", err)
|
||||||
if channel.EdgeRef != nil {
|
}
|
||||||
edgeConfigs[i] = *channel.EdgeRef
|
if exist == 0 {
|
||||||
|
return nil // 没有使用中通道,已经被清理过了
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备配置数据
|
||||||
|
edgeConfigs := make([]string, len(channels))
|
||||||
|
configs := make([]*g.PortConfigsReq, len(channels))
|
||||||
|
for i, channel := range channels {
|
||||||
|
if channel.EdgeRef != nil {
|
||||||
|
edgeConfigs[i] = *channel.EdgeRef
|
||||||
|
} else {
|
||||||
|
slog.Warn(fmt.Sprintf("通道 %d 没有保存节点引用", channel.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
configs[i] = &g.PortConfigsReq{
|
||||||
|
Status: false,
|
||||||
|
Port: int(channel.Port),
|
||||||
|
Edge: &[]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交配置
|
||||||
|
if env.RunMode == env.RunModeProd {
|
||||||
|
|
||||||
|
// 清空通道配置
|
||||||
|
secret := strings.Split(u.Z(proxy.Secret), ":")
|
||||||
|
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
||||||
|
err := gateway.GatewayPortConfigs(configs)
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开节点连接
|
||||||
|
_, err = g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
|
||||||
|
Uuid: proxy.Mac,
|
||||||
|
Edge: &edgeConfigs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("断开云平台连接失败", "error", err.Error())
|
||||||
|
return core.NewServErr("断开云平台连接失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for _, item := range configs {
|
||||||
|
str, _ := json.Marshal(item)
|
||||||
|
fmt.Println(string(str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pid = proxy.ID
|
||||||
} else {
|
} else {
|
||||||
slog.Warn(fmt.Sprintf("通道 %d 没有保存节点引用", channel.ID))
|
pid = *proxyId
|
||||||
}
|
}
|
||||||
|
|
||||||
configs[i] = &g.PortConfigsReq{
|
// 释放端口
|
||||||
Status: false,
|
err := freeChans(pid, batch)
|
||||||
Port: int(channel.Port),
|
|
||||||
Edge: &[]string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交配置
|
|
||||||
if env.RunMode == env.RunModeProd {
|
|
||||||
|
|
||||||
// 清空通道配置
|
|
||||||
secret := strings.Split(u.Z(proxy.Secret), ":")
|
|
||||||
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
|
||||||
err := gateway.GatewayPortConfigs(configs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断开节点连接
|
slog.Debug("清除代理端口配置", "proxy", pid, "batch", batch, "duration", time.Since(start).String())
|
||||||
_, err = g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
|
return nil
|
||||||
Uuid: proxy.Mac,
|
})
|
||||||
Edge: &edgeConfigs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("断开云平台连接失败", "error", err.Error())
|
|
||||||
return core.NewServErr("断开云平台连接失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
for _, item := range configs {
|
|
||||||
str, _ := json.Marshal(item)
|
|
||||||
fmt.Println(string(str))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放端口
|
|
||||||
err = freeChans(proxy.ID, batch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("清除代理端口配置", "proxy", proxy.IP.String(), "batch", batch, "duration", time.Since(start).String())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelBaiyinProvider) ClearExpiredChannels() error {
|
// ClearExpiredChannels 定期清理过期通道,返回清理数量
|
||||||
now := time.Now().Add(time.Hour)
|
// 通道有三种情况:
|
||||||
var batches []struct{ BatchNo string }
|
// - 过期等待清理,过期时间在一小时内,可以等待异步任务回收通道
|
||||||
err := q.Channel.Select(q.Channel.BatchNo).Where(q.Channel.ExpiredAt.Lt(now)).Group(q.Channel.BatchNo).Scan(&batches)
|
// - 过期未清理,过期时间超过一小时,说明异步任务可能失败了,需要强制清理
|
||||||
|
// - 异常通道,取用后任务失败,导致通道悬空,需要强制清理
|
||||||
|
func (s *channelBaiyinProvider) ClearExpiredChannels(proxyId int32) (int, error) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 获取使用中通道批次
|
||||||
|
keys, err := g.Redis.Keys(context.Background(), usedChansKey(proxyId, "*")).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("查询过期通道失败", err)
|
return 0, core.NewServErr("查询使用中通道失败", err)
|
||||||
|
}
|
||||||
|
batchList := make([]string, len(keys))
|
||||||
|
batchSet := make(map[string]struct{}, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
parts := strings.Split(key, ":")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return 0, core.NewServErr(fmt.Sprintf("使用中通道键格式错误: %s", key), nil)
|
||||||
|
}
|
||||||
|
batchList[i] = parts[3]
|
||||||
|
batchSet[parts[3]] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, batch := range batches {
|
// 排除未过期通道
|
||||||
err := s.RemoveChannels(batch.BatchNo)
|
var batchQueried []struct{ BatchNo string }
|
||||||
|
err = q.Channel.Debug().
|
||||||
|
Select(q.Channel.BatchNo).
|
||||||
|
Where(
|
||||||
|
q.Channel.BatchNo.In(batchList...),
|
||||||
|
q.Channel.ExpiredAt.Gte(now),
|
||||||
|
).
|
||||||
|
Group(q.Channel.BatchNo).
|
||||||
|
Scan(&batchQueried)
|
||||||
|
if err != nil {
|
||||||
|
return 0, core.NewServErr("查询过期通道失败", err)
|
||||||
|
}
|
||||||
|
for _, batch := range batchQueried {
|
||||||
|
delete(batchSet, batch.BatchNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理过期通道
|
||||||
|
slog.Info("批量清理过期通道", "count", len(batchSet))
|
||||||
|
for batchNo, _ := range batchSet {
|
||||||
|
err := s.RemoveChannels(batchNo, &proxyId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("清理过期通道失败", "batch", batch.BatchNo, "error", err)
|
slog.Error("清理过期通道失败", "batch", batchNo, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return len(batchSet), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func batchRemoveExpiredKey(bid string) string {
|
||||||
|
return fmt.Sprintf("platform:batch:remove_expired:%s", bid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAvailableEdges(gateway g.GatewayClient, filter *EdgeFilter, count int) ([]EdgeInfo, error) {
|
func getAvailableEdges(gateway g.GatewayClient, filter *EdgeFilter, count int) ([]EdgeInfo, error) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"platform/web/globals/orm"
|
"platform/web/globals/orm"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gen/field"
|
"gorm.io/gen/field"
|
||||||
@@ -26,7 +25,7 @@ func proxyStatusLockKey(id int32) string {
|
|||||||
|
|
||||||
func hasUsedChans(proxyID int32) (bool, error) {
|
func hasUsedChans(proxyID int32) (bool, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
pattern := usedChansKey + ":" + strconv.Itoa(int(proxyID)) + ":*"
|
pattern := usedChansKey(proxyID, "*")
|
||||||
keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result()
|
keys, _, err := g.Redis.Scan(ctx, 0, pattern, 1).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
|
func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
|
||||||
|
slog.Info("[event]尝试结束交易")
|
||||||
var event events.CloseTradeData
|
var event events.CloseTradeData
|
||||||
if err := json.Unmarshal(task.Payload(), &event); err != nil {
|
if err := json.Unmarshal(task.Payload(), &event); err != nil {
|
||||||
return fmt.Errorf("解析任务参数失败: %w", err)
|
return fmt.Errorf("解析任务参数失败: %w", err)
|
||||||
@@ -30,11 +31,11 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Trade.CompleteTrade(user, &data); err != nil {
|
if err := s.Trade.CompleteTrade(user, &data); err != nil {
|
||||||
slog.Debug("完成交易失败[异步结束订单]", "err", err)
|
slog.Debug("结束交易失败:完成交易失败", "err", err)
|
||||||
|
|
||||||
// 交易无法完成,关闭交易
|
// 交易无法完成,关闭交易
|
||||||
if err := s.Trade.CancelTrade(&data); err != nil {
|
if err := s.Trade.CancelTrade(&data); err != nil {
|
||||||
return fmt.Errorf("取消交易失败[异步结束订单]: %w", err)
|
return fmt.Errorf("结束交易失败:取消交易失败: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,17 +44,11 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) error {
|
|||||||
|
|
||||||
func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
|
func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
|
||||||
batch := string(task.Payload())
|
batch := string(task.Payload())
|
||||||
err = s.Channel.RemoveChannels(batch)
|
slog.Info("[event]删除通道", "batch", batch)
|
||||||
|
|
||||||
|
err = s.Channel.RemoveChannels(batch, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("删除通道失败: %w", err)
|
return fmt.Errorf("删除通道失败: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleClearExpiredChannels(_ context.Context, _ *asynq.Task) (err error) {
|
|
||||||
err = s.Channel.ClearExpiredChannels()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("清理过期通道失败: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
29
web/web.go
29
web/web.go
@@ -42,10 +42,6 @@ func RunApp(pCtx context.Context) error {
|
|||||||
return RunTask(ctx)
|
return RunTask(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
g.Go(func() error {
|
|
||||||
return RunCron(ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
return g.Wait()
|
return g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +91,6 @@ func RunTask(ctx context.Context) error {
|
|||||||
var mux = asynq.NewServeMux()
|
var mux = asynq.NewServeMux()
|
||||||
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
|
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
|
||||||
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
|
mux.HandleFunc(events.CloseTrade, tasks.HandleCompleteTrade)
|
||||||
mux.HandleFunc(events.ClearExpiredChannels, tasks.HandleClearExpiredChannels)
|
|
||||||
|
|
||||||
// 停止服务
|
// 停止服务
|
||||||
go func() {
|
go func() {
|
||||||
@@ -112,30 +107,6 @@ func RunTask(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunCron(ctx context.Context) error {
|
|
||||||
server := asynq.NewSchedulerFromRedisClient(deps.Redis, &asynq.SchedulerOpts{
|
|
||||||
Location: time.Local,
|
|
||||||
Logger: &AppAsynqLogger{},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 每小时清理一次一小时之前的过期通道
|
|
||||||
server.Register("0 * * * *", asynq.NewTask(events.ClearExpiredChannels, nil))
|
|
||||||
|
|
||||||
// 停止服务
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
server.Shutdown()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 启动服务
|
|
||||||
err := server.Run()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("定时任务服务运行失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppAsynqLogger struct{}
|
type AppAsynqLogger struct{}
|
||||||
|
|
||||||
func (l *AppAsynqLogger) Debug(args ...any) {
|
func (l *AppAsynqLogger) Debug(args ...any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user