重构消息队列连接管理,支持多数据源配置
主要变更: 1. 重构NATS、RabbitMQ和Redis连接管理模块,支持多数据源配置 2. 统一连接管理接口,增加数据源名称参数 3. 优化连接状态检查和错误处理 4. 增加连接池管理和资源清理机制 5. 改进日志输出格式和内容
This commit is contained in:
@@ -8,137 +8,160 @@ import (
|
|||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
nc *nats.Conn
|
muNats sync.RWMutex
|
||||||
js jetstream.JetStream
|
natsConns map[string]*nats.Conn // key: 数据源名称, value: NATS 连接
|
||||||
natsMu sync.RWMutex
|
natsJS map[string]nats.JetStreamContext // key: 数据源名称, value: JetStream 上下文
|
||||||
)
|
)
|
||||||
|
|
||||||
// natsConnect 建立 NATS 连接
|
func init() {
|
||||||
func natsConnect(ctx context.Context) error {
|
natsConns = make(map[string]*nats.Conn)
|
||||||
natsMu.Lock()
|
natsJS = make(map[string]nats.JetStreamContext)
|
||||||
defer natsMu.Unlock()
|
}
|
||||||
|
|
||||||
// 安全地关闭旧连接
|
// natsConnect 建立 NATS 连接
|
||||||
if oldConn := nc; oldConn != nil && !oldConn.IsClosed() {
|
func natsConnect(ctx context.Context, name string) error {
|
||||||
|
|
||||||
|
if g.Cfg().MustGet(ctx, "nats").IsEmpty() {
|
||||||
|
g.Log().Errorf(ctx, "❌ NATS 配置不存在")
|
||||||
|
return fmt.Errorf("NATS Configuration does not exist")
|
||||||
|
}
|
||||||
|
// 确定数据源名称
|
||||||
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "🔔 NATS [%s] 开始创建连接", dsName)
|
||||||
|
muNats.Lock()
|
||||||
|
defer muNats.Unlock()
|
||||||
|
|
||||||
|
// 安全地关闭旧连接(仅针对该数据源)
|
||||||
|
if oldConn, exists := natsConns[dsName]; exists && oldConn != nil && !oldConn.IsClosed() {
|
||||||
oldConn.Close()
|
oldConn.Close()
|
||||||
|
delete(natsConns, dsName)
|
||||||
|
delete(natsJS, dsName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从配置文件读取 NATS 地址
|
// 从配置文件读取 NATS 地址
|
||||||
natsURL := g.Cfg().MustGet(ctx, "nats.url").String()
|
natsURL := g.Cfg().MustGet(ctx, fmt.Sprintf("nats.%s.url", dsName)).String()
|
||||||
if natsURL == "" {
|
if natsURL == "" {
|
||||||
// 默认使用本地地址
|
// 默认使用本地地址
|
||||||
natsURL = nats.DefaultURL
|
natsURL = nats.DefaultURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用独立的日志上下文,避免使用外部可能被取消的上下文
|
|
||||||
logCtx := context.Background()
|
|
||||||
|
|
||||||
// 连接选项配置
|
// 连接选项配置
|
||||||
opts := []nats.Option{
|
opts := []nats.Option{
|
||||||
nats.Name("goframe-nats-client"),
|
nats.Name(fmt.Sprintf("goframe-nats-client-%s", dsName)),
|
||||||
nats.ReconnectWait(2 * time.Second),
|
nats.NoReconnect(),
|
||||||
nats.MaxReconnects(-1), // 无限重连
|
|
||||||
nats.PingInterval(10 * time.Second),
|
nats.PingInterval(10 * time.Second),
|
||||||
nats.MaxPingsOutstanding(5),
|
nats.MaxPingsOutstanding(5),
|
||||||
nats.ReconnectHandler(func(nc *nats.Conn) {
|
|
||||||
g.Log().Infof(logCtx, "✅ NATS 重连成功: %s", nc.ConnectedUrl())
|
|
||||||
|
|
||||||
natsMu.Lock()
|
|
||||||
defer natsMu.Unlock()
|
|
||||||
// 重新创建 JetStream 实例
|
|
||||||
if newJS, err := jetstream.New(nc); err == nil {
|
|
||||||
js = newJS
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
|
|
||||||
g.Log().Warningf(logCtx, "⚠️ NATS 连接断开: %v, 准备重连...", err)
|
|
||||||
}),
|
|
||||||
nats.ClosedHandler(func(nc *nats.Conn) {
|
nats.ClosedHandler(func(nc *nats.Conn) {
|
||||||
g.Log().Infof(logCtx, "NATS 连接已关闭: %s", nc.ConnectedUrl())
|
g.Log().Infof(ctx, "NATS [%s] 连接已关闭: %s", dsName, nc.ConnectedUrl())
|
||||||
}),
|
}),
|
||||||
nats.ErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) {
|
nats.ErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) {
|
||||||
g.Log().Errorf(logCtx, "NATS 错误: %v", err)
|
g.Log().Errorf(ctx, "❌ NATS [%s] 错误: %v", dsName, err)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
newConn, err := nats.Connect(natsURL, opts...)
|
||||||
nc, err = nats.Connect(natsURL, opts...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("NATS 连接失败: %w", err)
|
g.Log().Errorf(ctx, "❌ NATS [%s] 连接失败: %v", dsName, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待连接就绪
|
// 等待连接就绪
|
||||||
if nc.Status() != nats.CONNECTED {
|
if newConn.Status() != nats.CONNECTED {
|
||||||
select {
|
select {
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
// 连接超时,清理资源
|
// 连接超时,清理资源
|
||||||
if nc != nil {
|
newConn.Close()
|
||||||
nc.Close()
|
g.Log().Errorf(ctx, "❌ NATS [%s] 连接超时", dsName)
|
||||||
}
|
|
||||||
return fmt.Errorf("NATS 连接超时")
|
return fmt.Errorf("NATS 连接超时")
|
||||||
case <-nc.StatusChanged(nats.CONNECTED):
|
case <-newConn.StatusChanged(nats.CONNECTED):
|
||||||
// 连接成功
|
// 连接成功
|
||||||
|
g.Log().Infof(ctx, "✅ NATS [%s] 连接成功: %s", dsName, newConn.ConnectedUrl())
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// 外部上下文被取消,清理资源
|
// 外部上下文被取消,清理资源
|
||||||
if nc != nil {
|
newConn.Close()
|
||||||
nc.Close()
|
g.Log().Errorf(ctx, "NATS [%s] 连接被取消: %v", dsName, ctx.Err())
|
||||||
}
|
|
||||||
return fmt.Errorf("NATS 连接被取消: %w", ctx.Err())
|
return fmt.Errorf("NATS 连接被取消: %w", ctx.Err())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 JetStream 实例
|
// 创建 JetStream 实例
|
||||||
js, err = jetstream.New(nc)
|
newJS, err := newConn.JetStream(nats.MaxWait(10 * time.Second))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 创建 JetStream 失败,清理连接
|
// 创建 JetStream 失败,清理连接
|
||||||
if nc != nil {
|
newConn.Close()
|
||||||
nc.Close()
|
g.Log().Errorf(ctx, "❌ NATS [%s] 创建 JetStream 失败: %v", dsName, err)
|
||||||
}
|
return err
|
||||||
return fmt.Errorf("创建 JetStream 失败: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Log().Infof(ctx, "✅ NATS 连接成功: %s", nc.ConnectedUrl())
|
// 保存连接和 JetStream 上下文
|
||||||
|
natsConns[dsName] = newConn
|
||||||
|
natsJS[dsName] = newJS
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// natsPing 检测 NATS 连接状态
|
// natsPing 检测 NATS 连接状态
|
||||||
func natsPing() bool {
|
func natsPing(ctx context.Context, name string) bool {
|
||||||
natsMu.RLock()
|
// 确定数据源名称
|
||||||
defer natsMu.RUnlock()
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
if nc == nil || nc.IsClosed() {
|
dsName = name
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 NATS 的状态检查
|
muNats.RLock()
|
||||||
if nc.Status() != nats.CONNECTED {
|
defer muNats.RUnlock()
|
||||||
|
|
||||||
|
nc, exists := natsConns[dsName]
|
||||||
|
if !exists || nc == nil || nc.IsClosed() || nc.Status() != nats.CONNECTED {
|
||||||
|
g.Log().Errorf(ctx, "❌ NATS [%s] 连接已关闭或不可用", dsName)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
g.Log().Infof(ctx, "📊 NATS [%s] 连接正常: %s", dsName, nc.ConnectedUrl())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// natsReconnect 重连 NATS
|
|
||||||
func natsReconnect(ctx context.Context) error {
|
|
||||||
if err := natsConnect(ctx); err != nil {
|
|
||||||
return fmt.Errorf("nats重连失败: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// natsClose 关闭 NATS 连接
|
// natsClose 关闭 NATS 连接
|
||||||
func natsClose(ctx context.Context) error {
|
func natsClose(ctx context.Context, name string) error {
|
||||||
natsMu.Lock()
|
// 确定数据源名称
|
||||||
defer natsMu.Unlock()
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
if nc == nil || nc.IsClosed() {
|
dsName = name
|
||||||
return nil // 连接已经关闭或不存在
|
|
||||||
}
|
}
|
||||||
nc.Close()
|
|
||||||
g.Log().Infof(ctx, "✅ NATS 连接已关闭")
|
muNats.Lock()
|
||||||
|
defer muNats.Unlock()
|
||||||
|
|
||||||
|
if nc, exists := natsConns[dsName]; exists && nc != nil && !nc.IsClosed() {
|
||||||
|
nc.Close()
|
||||||
|
}
|
||||||
|
delete(natsConns, dsName)
|
||||||
|
delete(natsJS, dsName)
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "✅ NATS [%s] 连接已关闭", dsName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNatsConn 获取 NATS 连接(内部使用)
|
||||||
|
func getNatsConn(name string) *nats.Conn {
|
||||||
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
|
}
|
||||||
|
return natsConns[dsName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNatsJS 获取 JetStream 上下文(内部使用)
|
||||||
|
func getNatsJS(name string) nats.JetStreamContext {
|
||||||
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
|
}
|
||||||
|
return natsJS[dsName]
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,103 +7,158 @@ import (
|
|||||||
"github.com/gogf/gf/v2/util/gconv"
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
amqp "github.com/rabbitmq/amqp091-go"
|
amqp "github.com/rabbitmq/amqp091-go"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
conn *amqp.Connection
|
muRabbitMQ sync.RWMutex
|
||||||
channel *amqp.Channel
|
rabbitmqConns map[string]*amqp.Connection
|
||||||
rabbitmqMu sync.RWMutex
|
rabbitmqChannels map[string]*amqp.Channel
|
||||||
)
|
)
|
||||||
|
|
||||||
// config RabbitMQ 配置
|
func init() {
|
||||||
type config struct {
|
rabbitmqConns = make(map[string]*amqp.Connection)
|
||||||
Host string
|
rabbitmqChannels = make(map[string]*amqp.Channel)
|
||||||
Port int
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
VHost string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rabbitmqConnect(ctx context.Context) error {
|
// rabbitmqConnect 建立 RabbitMQ 连接
|
||||||
rabbitmqMu.Lock()
|
func rabbitmqConnect(ctx context.Context, name string) error {
|
||||||
defer rabbitmqMu.Unlock()
|
if g.Cfg().MustGet(ctx, "rabbitmq").IsEmpty() {
|
||||||
|
g.Log().Errorf(ctx, "❌ RabbitMQ 配置不存在")
|
||||||
LOOP:
|
return fmt.Errorf("RabbitMQ Configuration does not exist")
|
||||||
cfg := &config{
|
}
|
||||||
Host: g.Cfg().MustGet(ctx, "rabbitmq.host").String(),
|
// 确定数据源名称
|
||||||
Port: g.Cfg().MustGet(ctx, "rabbitmq.port").Int(),
|
dsName := "default"
|
||||||
Username: g.Cfg().MustGet(ctx, "rabbitmq.username").String(),
|
if !g.IsEmpty(name) {
|
||||||
Password: g.Cfg().MustGet(ctx, "rabbitmq.password").String(),
|
dsName = name
|
||||||
VHost: g.Cfg().MustGet(ctx, "rabbitmq.vhost", "/").String(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url := "amqp://" + cfg.Username + ":" + cfg.Password + "@" + cfg.Host + ":" + gconv.String(cfg.Port) + "/" + cfg.VHost
|
g.Log().Infof(ctx, "🔔 RabbitMQ [%s] 开始创建连接", dsName)
|
||||||
|
muRabbitMQ.Lock()
|
||||||
|
defer muRabbitMQ.Unlock()
|
||||||
|
|
||||||
var err error
|
// 安全地关闭旧连接(仅针对该数据源)
|
||||||
conn, err = amqp.Dial(url)
|
if oldConn, exists := rabbitmqConns[dsName]; exists && oldConn != nil && !oldConn.IsClosed() {
|
||||||
|
oldConn.Close()
|
||||||
|
}
|
||||||
|
if oldChannel, exists := rabbitmqChannels[dsName]; exists && oldChannel != nil && !oldChannel.IsClosed() {
|
||||||
|
oldChannel.Close()
|
||||||
|
}
|
||||||
|
delete(rabbitmqConns, dsName)
|
||||||
|
delete(rabbitmqChannels, dsName)
|
||||||
|
|
||||||
|
// 从配置文件读取 RabbitMQ 配置
|
||||||
|
host := g.Cfg().MustGet(ctx, fmt.Sprintf("rabbitmq.%s.host", dsName)).String()
|
||||||
|
port := g.Cfg().MustGet(ctx, fmt.Sprintf("rabbitmq.%s.port", dsName)).Int()
|
||||||
|
username := g.Cfg().MustGet(ctx, fmt.Sprintf("rabbitmq.%s.username", dsName)).String()
|
||||||
|
password := g.Cfg().MustGet(ctx, fmt.Sprintf("rabbitmq.%s.password", dsName)).String()
|
||||||
|
vHost := g.Cfg().MustGet(ctx, fmt.Sprintf("rabbitmq.%s.vhost", dsName), "/").String()
|
||||||
|
if g.IsEmpty(host) {
|
||||||
|
return fmt.Errorf("❌ RabbitMQ 配置错误: host 不能为空 (数据源: %s)", dsName)
|
||||||
|
}
|
||||||
|
if g.IsEmpty(port) {
|
||||||
|
return fmt.Errorf("❌ RabbitMQ 配置错误: port 不能为空 (数据源: %s)", dsName)
|
||||||
|
}
|
||||||
|
if g.IsEmpty(username) {
|
||||||
|
return fmt.Errorf("❌ RabbitMQ 配置错误: username 不能为空 (数据源: %s)", dsName)
|
||||||
|
}
|
||||||
|
if g.IsEmpty(password) {
|
||||||
|
return fmt.Errorf("❌ RabbitMQ 配置错误: password 不能为空 (数据源: %s)", dsName)
|
||||||
|
}
|
||||||
|
// 构建连接 URL
|
||||||
|
url := "amqp://" + username + ":" + password + "@" + host + ":" + gconv.String(port) + "/" + vHost
|
||||||
|
|
||||||
|
// 创建连接
|
||||||
|
newConn, err := amqp.Dial(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Errorf(ctx, "重连失败: %v", err)
|
g.Log().Errorf(ctx, "❌ RabbitMQ [%s] 连接失败: %v", dsName, err)
|
||||||
|
return err
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
goto LOOP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channel, err = conn.Channel()
|
// 创建 Channel
|
||||||
|
newChannel, err := newConn.Channel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Errorf(ctx, "创建 Channel 失败: %v", err)
|
g.Log().Errorf(ctx, "❌ RabbitMQ [%s] 创建 Channel 失败: %v", dsName, err)
|
||||||
|
newConn.Close()
|
||||||
time.Sleep(2 * time.Second)
|
return err
|
||||||
goto LOOP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Log().Info(ctx, "RabbitMQ 重连成功")
|
// 保存连接和 Channel
|
||||||
return nil
|
rabbitmqConns[dsName] = newConn
|
||||||
}
|
rabbitmqChannels[dsName] = newChannel
|
||||||
|
|
||||||
// rabbitmqReconnect 重新连接
|
g.Log().Infof(ctx, "✅ RabbitMQ [%s] 连接成功", dsName)
|
||||||
func rabbitmqReconnect(ctx context.Context) error {
|
|
||||||
if err := rabbitmqConnect(ctx); err != nil {
|
|
||||||
return fmt.Errorf("nats重连失败: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// rabbitmqPing 检测 RabbitMQ 连接状态
|
// rabbitmqPing 检测 RabbitMQ 连接状态
|
||||||
func rabbitmqPing() bool {
|
func rabbitmqPing(ctx context.Context, name string) bool {
|
||||||
rabbitmqMu.RLock()
|
// 确定数据源名称
|
||||||
defer rabbitmqMu.RUnlock()
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
|
}
|
||||||
|
|
||||||
if conn == nil || conn.IsClosed() {
|
muRabbitMQ.RLock()
|
||||||
|
defer muRabbitMQ.RUnlock()
|
||||||
|
|
||||||
|
conn, exists := rabbitmqConns[dsName]
|
||||||
|
channel, channelExists := rabbitmqChannels[dsName]
|
||||||
|
if !exists || conn == nil || conn.IsClosed() || !channelExists || channel == nil || channel.IsClosed() {
|
||||||
|
g.Log().Errorf(ctx, "❌ RabbitMQ [%s] 连接已关闭或不可用", dsName)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "📊 RabbitMQ [%s] 连接正常", dsName)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// rabbitmqClose 关闭连接
|
// rabbitmqClose 关闭 RabbitMQ 连接
|
||||||
func rabbitmqClose(ctx context.Context) error {
|
func rabbitmqClose(ctx context.Context, name string) error {
|
||||||
rabbitmqMu.Lock()
|
// 确定数据源名称
|
||||||
defer rabbitmqMu.Unlock()
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
muRabbitMQ.Lock()
|
||||||
|
defer muRabbitMQ.Unlock()
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
if channel != nil {
|
if channel, exists := rabbitmqChannels[dsName]; exists && channel != nil && !channel.IsClosed() {
|
||||||
if err := channel.Close(); err != nil {
|
if err := channel.Close(); err != nil {
|
||||||
g.Log().Errorf(ctx, "关闭 RabbitMQ Channel 失败: %v", err)
|
g.Log().Errorf(ctx, "❌ RabbitMQ [%s] 关闭 Channel 失败: %v", dsName, err)
|
||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
channel = nil
|
|
||||||
}
|
}
|
||||||
|
delete(rabbitmqChannels, dsName)
|
||||||
|
|
||||||
if conn != nil {
|
if conn, exists := rabbitmqConns[dsName]; exists && conn != nil && !conn.IsClosed() {
|
||||||
if err := conn.Close(); err != nil {
|
if err := conn.Close(); err != nil {
|
||||||
g.Log().Errorf(ctx, "关闭 RabbitMQ 连接失败: %v", err)
|
g.Log().Errorf(ctx, "❌ RabbitMQ [%s] 关闭连接失败: %v", dsName, err)
|
||||||
lastErr = err
|
lastErr = err
|
||||||
}
|
}
|
||||||
conn = nil
|
|
||||||
}
|
}
|
||||||
|
delete(rabbitmqConns, dsName)
|
||||||
|
|
||||||
g.Log().Info(ctx, "RabbitMQ 连接已关闭")
|
g.Log().Infof(ctx, "✅ RabbitMQ [%s] 连接已关闭", dsName)
|
||||||
return lastErr
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRabbitMQConn 获取 RabbitMQ 连接(内部使用)
|
||||||
|
func getRabbitMQConn(name string) *amqp.Connection {
|
||||||
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
|
}
|
||||||
|
return rabbitmqConns[dsName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRabbitMQChannel 获取 RabbitMQ Channel(内部使用)
|
||||||
|
func getRabbitMQChannel(name string) *amqp.Channel {
|
||||||
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
|
}
|
||||||
|
return rabbitmqChannels[dsName]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Redis 数据源连接管理
|
// Redis 连接管理
|
||||||
// 负责 Redis 数据源的连接、重连、健康检查和优雅关闭
|
// 负责 Redis 的连接、重连、健康检查和优雅关闭
|
||||||
// 支持多数据源和无限重连
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
package message
|
package message
|
||||||
@@ -9,477 +8,191 @@ package message
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gredis"
|
"github.com/gogf/gf/v2/database/gredis"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/glog"
|
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
var (
|
||||||
// Redis 数据源配置结构
|
muRedis sync.RWMutex
|
||||||
// =============================================================================
|
redisConns map[string]*gredis.Redis
|
||||||
|
redisConfigs map[string]*gredis.Config
|
||||||
|
)
|
||||||
|
|
||||||
type redisDataSourceConfig struct {
|
func init() {
|
||||||
name string // 数据源名称
|
redisConns = make(map[string]*gredis.Redis)
|
||||||
address string // Redis 地址,如: 127.0.0.1:6379
|
redisConfigs = make(map[string]*gredis.Config)
|
||||||
db int // 数据库编号
|
|
||||||
pass string // 密码
|
|
||||||
maxRetries int // 最大重试次数,-1 表示无限重试
|
|
||||||
retryInterval time.Duration // 重试间隔
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// redisConnect 建立 Redis 连接
|
||||||
// Redis 数据源接口
|
// name: 数据源名称,如果为空则使用默认数据源
|
||||||
// =============================================================================
|
func redisConnect(ctx context.Context, name string) error {
|
||||||
|
if g.Cfg().MustGet(ctx, "redis").IsEmpty() {
|
||||||
type redisDataSource interface {
|
g.Log().Errorf(ctx, "❌ Redis 配置不存在")
|
||||||
name() string
|
return fmt.Errorf("redis Configuration does not exist")
|
||||||
getClient() *gredis.Redis
|
|
||||||
getIsConnected() bool
|
|
||||||
redisConnect(ctx context.Context) error
|
|
||||||
redisReconnect(ctx context.Context) error
|
|
||||||
redisClose(ctx context.Context) error
|
|
||||||
redisPing(ctx context.Context) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Redis 数据源实现
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
type baseRedisDataSource struct {
|
|
||||||
config *redisDataSourceConfig
|
|
||||||
client *gredis.Redis
|
|
||||||
isConnected bool
|
|
||||||
mu sync.RWMutex
|
|
||||||
lastError error
|
|
||||||
lastErrorTime time.Time
|
|
||||||
reconnectMu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBaseRedisDataSource(config *redisDataSourceConfig) *baseRedisDataSource {
|
|
||||||
return &baseRedisDataSource{
|
|
||||||
config: config,
|
|
||||||
isConnected: false,
|
|
||||||
}
|
}
|
||||||
}
|
// 确定数据源名称
|
||||||
|
dsName := "default"
|
||||||
func (d *baseRedisDataSource) name() string {
|
if !g.IsEmpty(name) {
|
||||||
return d.config.name
|
dsName = name
|
||||||
}
|
|
||||||
|
|
||||||
func (d *baseRedisDataSource) getClient() *gredis.Redis {
|
|
||||||
d.mu.RLock()
|
|
||||||
defer d.mu.RUnlock()
|
|
||||||
return d.client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *baseRedisDataSource) getIsConnected() bool {
|
|
||||||
d.mu.RLock()
|
|
||||||
defer d.mu.RUnlock()
|
|
||||||
return d.isConnected && d.client != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *baseRedisDataSource) redisConnect(ctx context.Context) error {
|
|
||||||
// 使用互斥锁防止并发重连
|
|
||||||
d.reconnectMu.Lock()
|
|
||||||
defer d.reconnectMu.Unlock()
|
|
||||||
|
|
||||||
d.mu.Lock()
|
|
||||||
if d.client != nil {
|
|
||||||
d.client.Close(ctx)
|
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "🔔 Redis [%s] 开始创建连接", dsName)
|
||||||
|
muRedis.Lock()
|
||||||
|
defer muRedis.Unlock()
|
||||||
|
|
||||||
|
// 安全地关闭旧连接(仅针对该数据源)
|
||||||
|
if oldRedis, exists := redisConns[dsName]; exists && oldRedis != nil {
|
||||||
|
oldRedis.Close(ctx)
|
||||||
|
delete(redisConns, dsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从配置文件读取 Redis 配置
|
||||||
|
redisAddr := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.address", dsName)).String()
|
||||||
|
if g.IsEmpty(redisAddr) {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis 配置错误: address 不能为空 (数据源: %s)", dsName)
|
||||||
|
return fmt.Errorf("❌ Redis 配置错误: address 不能为空 (数据源: %s)", dsName)
|
||||||
|
}
|
||||||
|
redisDB := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.db", dsName)).Int()
|
||||||
|
if redisDB < 0 || redisDB > 15 {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis 配置错误: db 必须在 0-15 之间 (当前值: %d)", redisDB)
|
||||||
|
return fmt.Errorf("❌ Redis 配置错误: db 必须在 0-15 之间 (当前值: %d)", redisDB)
|
||||||
|
}
|
||||||
|
idleTimeout := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.idleTimeout", dsName)).String()
|
||||||
|
redisIdleTimeout, err := time.ParseDuration(idleTimeout)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis idleTimeout 格式错误: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maxConnLifetime := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.maxConnLifetime", dsName)).String()
|
||||||
|
redisMaxConnLifetime, err := time.ParseDuration(maxConnLifetime)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis maxConnLifetime 格式错误: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
waitTimeout := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.waitTimeout", dsName)).String()
|
||||||
|
redisWaitTimeout, err := time.ParseDuration(waitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis waitTimeout 格式错误: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dialTimeout := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.dialTimeout", dsName)).String()
|
||||||
|
redisDialTimeout, err := time.ParseDuration(dialTimeout)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis dialTimeout 格式错误: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
readTimeout := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.readTimeout", dsName)).String()
|
||||||
|
redisReadTimeout, err := time.ParseDuration(readTimeout)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis readTimeout 格式错误: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
writeTimeout := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.writeTimeout", dsName)).String()
|
||||||
|
redisWriteTimeout, err := time.ParseDuration(writeTimeout)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis writeTimeout 格式错误: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maxActive := g.Cfg().MustGet(ctx, fmt.Sprintf("redis.%s.maxActive", dsName)).Int()
|
||||||
|
if g.IsEmpty(maxActive) {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis maxActive 配置错误: %v", maxActive)
|
||||||
|
return fmt.Errorf("❌ Redis maxActive 配置错误")
|
||||||
|
}
|
||||||
// 构建 GoFrame Redis 配置
|
// 构建 GoFrame Redis 配置
|
||||||
redisConfig := &gredis.Config{
|
redisConfig := &gredis.Config{
|
||||||
Address: d.config.address,
|
Address: redisAddr,
|
||||||
Db: d.config.db,
|
Db: redisDB,
|
||||||
Pass: d.config.pass,
|
IdleTimeout: redisIdleTimeout,
|
||||||
|
MaxConnLifetime: redisMaxConnLifetime,
|
||||||
|
WaitTimeout: redisWaitTimeout,
|
||||||
|
DialTimeout: redisDialTimeout,
|
||||||
|
ReadTimeout: redisReadTimeout,
|
||||||
|
WriteTimeout: redisWriteTimeout,
|
||||||
|
MaxActive: maxActive,
|
||||||
}
|
}
|
||||||
|
redisConfigs[dsName] = redisConfig
|
||||||
|
|
||||||
// 使用 GoFrame 的 Redis 连接
|
// 使用 GoFrame 的 Redis 连接
|
||||||
redisObj, err := gredis.New(redisConfig)
|
newRedis, err := gredis.New(redisConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.mu.Lock()
|
g.Log().Errorf(ctx, "❌ Redis [%s] 连接失败: %v", dsName, err)
|
||||||
d.isConnected = false
|
return err
|
||||||
d.lastError = err
|
}
|
||||||
d.lastErrorTime = time.Now()
|
// 测试连接(直接调用避免死锁)
|
||||||
d.mu.Unlock()
|
_, err = newRedis.Do(ctx, "PING")
|
||||||
return fmt.Errorf("datasource [%s] connection failed: %w", d.config.name, err)
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis [%s] 连接失败: ping 失败 - %v", dsName, err)
|
||||||
|
_ = newRedis.Close(ctx)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mu.Lock()
|
redisConns[dsName] = newRedis
|
||||||
d.client = redisObj
|
g.Log().Infof(ctx, "✅ Redis [%s] 连接成功: %s (DB: %d)", dsName, redisAddr, redisDB)
|
||||||
d.mu.Unlock()
|
|
||||||
|
|
||||||
// 测试连接
|
|
||||||
if !d.redisPing(ctx) {
|
|
||||||
d.mu.Lock()
|
|
||||||
d.isConnected = false
|
|
||||||
d.lastError = err
|
|
||||||
d.lastErrorTime = time.Now()
|
|
||||||
d.mu.Unlock()
|
|
||||||
return fmt.Errorf("datasource [%s] ping failed: %w", d.config.name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.mu.Lock()
|
|
||||||
d.isConnected = true
|
|
||||||
d.lastError = nil
|
|
||||||
d.mu.Unlock()
|
|
||||||
glog.Infof(ctx, "✅ datasource [%s] connected successfully", d.config.name)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *baseRedisDataSource) redisReconnect(ctx context.Context) error {
|
// redisPing 检测 Redis 连接状态(带超时保护)
|
||||||
glog.Infof(ctx, "🔄 reconnecting datasource [%s]", d.config.name)
|
func redisPing(ctx context.Context, name string) bool {
|
||||||
return d.redisConnect(ctx)
|
// 确定数据源名称
|
||||||
}
|
dsName := "default"
|
||||||
|
if !g.IsEmpty(name) {
|
||||||
func (d *baseRedisDataSource) redisClose(ctx context.Context) error {
|
dsName = name
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
|
|
||||||
if d.client != nil {
|
|
||||||
if err := d.client.Close(ctx); err != nil {
|
|
||||||
return fmt.Errorf("datasource [%s] close failed: %w", d.config.name, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
d.isConnected = false
|
muRedis.RLock()
|
||||||
glog.Infof(ctx, "datasource [%s] closed", d.config.name)
|
defer muRedis.RUnlock()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *baseRedisDataSource) redisPing(ctx context.Context) bool {
|
rc, exists := redisConns[dsName]
|
||||||
d.mu.RLock()
|
if !exists || rc == nil {
|
||||||
client := d.client
|
g.Log().Errorf(ctx, "❌ Redis [%s] 连接未建立", dsName)
|
||||||
d.mu.RUnlock()
|
|
||||||
|
|
||||||
if client == nil {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := client.Do(ctx, "PING")
|
// 创建带超时的子上下文,避免死锁
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := rc.Do(timeoutCtx, "PING")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis [%s] ping 失败: %v", dsName, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "📊 Redis [%s] 连接正常", dsName)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// redisClose 关闭 Redis 连接
|
||||||
// Redis 多数据源管理器
|
func redisClose(ctx context.Context, name string) error {
|
||||||
// =============================================================================
|
// 确定数据源名称
|
||||||
|
dsName := "default"
|
||||||
type redisDataSourceManager struct {
|
if !g.IsEmpty(name) {
|
||||||
sources map[string]redisDataSource
|
dsName = name
|
||||||
mu sync.RWMutex
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
started bool
|
|
||||||
maxRetries int
|
|
||||||
reconnectCh chan string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
globalRedisManager *redisDataSourceManager
|
|
||||||
redisManagerOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
// getRedisManager 获取全局 Redis 管理器
|
|
||||||
func getRedisManager() *redisDataSourceManager {
|
|
||||||
redisManagerOnce.Do(func() {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
globalRedisManager = &redisDataSourceManager{
|
|
||||||
sources: make(map[string]redisDataSource),
|
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
started: false,
|
|
||||||
maxRetries: -1, // 默认无限重试
|
|
||||||
reconnectCh: make(chan string, 100),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return globalRedisManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerDataSource 注册 Redis 数据源
|
|
||||||
func (m *redisDataSourceManager) registerDataSource(config *redisDataSourceConfig) error {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if _, exists := m.sources[config.name]; exists {
|
|
||||||
return fmt.Errorf("datasource [%s] already exists", config.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
source := newBaseRedisDataSource(config)
|
muRedis.Lock()
|
||||||
m.sources[config.name] = source
|
defer muRedis.Unlock()
|
||||||
|
|
||||||
|
if rc, exists := redisConns[dsName]; exists && rc != nil {
|
||||||
|
if err := rc.Close(ctx); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis [%s] 关闭失败: %v", dsName, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(redisConns, dsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "✅ Redis [%s] 连接已关闭", dsName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDataSource 获取 Redis 数据源
|
// getRedisConn 获取 Redis 连接(内部使用)
|
||||||
func (m *redisDataSourceManager) getDataSource(name string) (redisDataSource, error) {
|
func getRedisConn(name string) *gredis.Redis {
|
||||||
m.mu.RLock()
|
dsName := "default"
|
||||||
defer m.mu.RUnlock()
|
if !g.IsEmpty(name) {
|
||||||
|
dsName = name
|
||||||
source, exists := m.sources[name]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("datasource [%s] not found", name)
|
|
||||||
}
|
}
|
||||||
return source, nil
|
return redisConns[dsName]
|
||||||
}
|
|
||||||
|
|
||||||
// getAllDataSourceNames 获取所有 Redis 数据源名称
|
|
||||||
func (m *redisDataSourceManager) getAllDataSourceNames() []string {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
names := make([]string, 0, len(m.sources))
|
|
||||||
for name := range m.sources {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// initializeFromConfig 从配置初始化 Redis 数据源
|
|
||||||
func (m *redisDataSourceManager) initializeFromConfig(ctx context.Context) error {
|
|
||||||
var firstErr error
|
|
||||||
|
|
||||||
// 获取 redis 配置下的所有子键
|
|
||||||
redisConfig := g.Cfg().MustGet(ctx, "redis")
|
|
||||||
if redisConfig.IsNil() {
|
|
||||||
glog.Warningf(ctx, "no redis configuration found in config.yml")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将配置转换为 map
|
|
||||||
configMap := redisConfig.Map()
|
|
||||||
if configMap == nil {
|
|
||||||
glog.Warningf(ctx, "redis configuration is not a map")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历所有 redis 子配置
|
|
||||||
for name, subConfig := range configMap {
|
|
||||||
// 跳过非对象类型的配置
|
|
||||||
subMap, ok := subConfig.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有 address 配置
|
|
||||||
address, hasAddress := subMap["address"]
|
|
||||||
if !hasAddress || gconv.String(address) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建数据源配置
|
|
||||||
config := &redisDataSourceConfig{
|
|
||||||
name: name,
|
|
||||||
address: gconv.String(address),
|
|
||||||
db: gconv.Int(subMap["db"]),
|
|
||||||
pass: gconv.String(subMap["pass"]),
|
|
||||||
maxRetries: gconv.Int(subMap["maxRetries"]),
|
|
||||||
retryInterval: gconv.Duration(subMap["retryInterval"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认值
|
|
||||||
if config.maxRetries == 0 {
|
|
||||||
config.maxRetries = -1 // 默认无限重试
|
|
||||||
}
|
|
||||||
if config.retryInterval == 0 {
|
|
||||||
config.retryInterval = 5 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册数据源
|
|
||||||
if err := m.registerDataSource(config); err != nil {
|
|
||||||
glog.Errorf(ctx, "failed to register datasource [%s]: %v", name, err)
|
|
||||||
if firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接数据源
|
|
||||||
source, _ := m.getDataSource(name)
|
|
||||||
if err := source.redisConnect(ctx); err != nil {
|
|
||||||
glog.Errorf(ctx, "failed to initialize datasource [%s]: %v", name, err)
|
|
||||||
if firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// startHealthCheck 启动健康检查
|
|
||||||
func (m *redisDataSourceManager) startHealthCheck() {
|
|
||||||
if m.started {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.started = true
|
|
||||||
|
|
||||||
// 启动健康检查循环
|
|
||||||
go m.healthCheckLoop()
|
|
||||||
|
|
||||||
// 启动重连处理循环
|
|
||||||
go m.reconnectLoop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// healthCheckLoop 健康检查循环
|
|
||||||
func (m *redisDataSourceManager) healthCheckLoop() {
|
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-m.ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
m.checkConnections()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconnectLoop 重连处理循环
|
|
||||||
func (m *redisDataSourceManager) reconnectLoop() {
|
|
||||||
reconnectCounts := make(map[string]int)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-m.ctx.Done():
|
|
||||||
return
|
|
||||||
case name := <-m.reconnectCh:
|
|
||||||
go func(dsName string) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
source, err := m.getDataSource(dsName)
|
|
||||||
if err != nil {
|
|
||||||
glog.Errorf(ctx, "datasource [%s] not found for reconnect", dsName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := source.redisReconnect(ctx); err != nil {
|
|
||||||
glog.Errorf(ctx, "datasource [%s] reconnect failed: %v", dsName, err)
|
|
||||||
|
|
||||||
// 记录重连次数
|
|
||||||
reconnectCounts[dsName]++
|
|
||||||
|
|
||||||
// 检查重连次数限制
|
|
||||||
if m.maxRetries > 0 && reconnectCounts[dsName] > m.maxRetries {
|
|
||||||
glog.Errorf(ctx, "datasource [%s] reconnect count %d exceeds limit %d, stopping auto-reconnect",
|
|
||||||
dsName, reconnectCounts[dsName], m.maxRetries)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟后重新放入重连队列
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
select {
|
|
||||||
case m.reconnectCh <- dsName:
|
|
||||||
default:
|
|
||||||
// 通道已满,丢弃通知
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 重连成功,重置计数器
|
|
||||||
reconnectCounts[dsName] = 0
|
|
||||||
}
|
|
||||||
}(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkConnections 检查连接状态
|
|
||||||
func (m *redisDataSourceManager) checkConnections() {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
for name, source := range m.sources {
|
|
||||||
if !source.getIsConnected() {
|
|
||||||
glog.Warningf(context.Background(), "datasource [%s] disconnected, queued for reconnect", name)
|
|
||||||
|
|
||||||
// 发送到重连队列
|
|
||||||
select {
|
|
||||||
case m.reconnectCh <- name:
|
|
||||||
default:
|
|
||||||
// 通道已满,丢弃通知
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeAll 关闭所有 Redis 数据源
|
|
||||||
func (m *redisDataSourceManager) closeAll(ctx context.Context) error {
|
|
||||||
m.cancel()
|
|
||||||
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for name, source := range m.sources {
|
|
||||||
if err := source.redisClose(ctx); err != nil {
|
|
||||||
glog.Errorf(ctx, "failed to close datasource [%s]: %v", name, err)
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 全局初始化
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
var (
|
|
||||||
redisManager = getRedisManager()
|
|
||||||
)
|
|
||||||
|
|
||||||
// init 初始化 Redis 数据源
|
|
||||||
func init() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// 从配置初始化多数据源
|
|
||||||
if err := redisManager.initializeFromConfig(ctx); err != nil {
|
|
||||||
glog.Errorf(ctx, "❌ Failed to initialize Redis datasources: %v", err)
|
|
||||||
} else {
|
|
||||||
glog.Infof(ctx, "✅ Redis datasources initialized: %v", redisManager.getAllDataSourceNames())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动健康检查
|
|
||||||
redisManager.startHealthCheck()
|
|
||||||
|
|
||||||
// 设置优雅关闭
|
|
||||||
setupGracefulShutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupGracefulShutdown 设置优雅关闭
|
|
||||||
func setupGracefulShutdown() {
|
|
||||||
go func() {
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-sigCh
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
glog.Info(ctx, "🔄 Shutting down Redis connections...")
|
|
||||||
if err := redisManager.closeAll(ctx); err != nil {
|
|
||||||
glog.Errorf(ctx, "❌ Failed to close Redis connections: %v", err)
|
|
||||||
} else {
|
|
||||||
glog.Info(ctx, "✅ Redis connections closed successfully")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 私有辅助函数
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// getDefaultDataSource 获取默认数据源
|
|
||||||
func getDefaultDataSource() (redisDataSource, error) {
|
|
||||||
return redisManager.getDataSource("default")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ type messagePublishConfig interface {
|
|||||||
GetPublishMsgType()
|
GetPublishMsgType()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type messagePublishDelayConfig interface {
|
||||||
|
GetPublishDelayMsgType()
|
||||||
|
}
|
||||||
|
|
||||||
type messageSubscribeConfig interface {
|
type messageSubscribeConfig interface {
|
||||||
GetSubscribeMsgType()
|
GetSubscribeMsgType()
|
||||||
}
|
}
|
||||||
@@ -15,12 +19,14 @@ type messageSubscribeConfig interface {
|
|||||||
type messageUtil interface {
|
type messageUtil interface {
|
||||||
// Publish 发布消息
|
// Publish 发布消息
|
||||||
Publish(ctx context.Context, msg messagePublishConfig) error
|
Publish(ctx context.Context, msg messagePublishConfig) error
|
||||||
|
// PublishDelay 发布延迟消息
|
||||||
|
PublishDelay(ctx context.Context, msg messagePublishDelayConfig) error
|
||||||
// Subscribe 订阅消息
|
// Subscribe 订阅消息
|
||||||
Subscribe(ctx context.Context, msg messageSubscribeConfig) error
|
Subscribe(ctx context.Context, msg messageSubscribeConfig) error
|
||||||
// Ping 检测连接状态
|
// Ping 检测连接状态
|
||||||
ping(ctx context.Context) bool
|
Ping(ctx context.Context) bool
|
||||||
// Reconnect 重连
|
// Connect 连接
|
||||||
reconnect(ctx context.Context) error
|
Connect(ctx context.Context) error
|
||||||
// Close 关闭连接
|
// Close 关闭连接
|
||||||
close(ctx context.Context) error
|
Close(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package message
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessageType 消息队列类型
|
// MessageType 消息队列类型
|
||||||
@@ -32,7 +32,6 @@ type pluginManager struct {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
defaultPluginManager = newPluginManager()
|
defaultPluginManager = newPluginManager()
|
||||||
// 不再支持默认插件类型,必须显式指定类型
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// newPluginManager 创建插件管理器
|
// newPluginManager 创建插件管理器
|
||||||
@@ -42,63 +41,6 @@ func newPluginManager() *pluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterPlugin 注册消息队列插件
|
|
||||||
// 所有插件必须通过此方法注册,自动进行连接检测
|
|
||||||
// 只有连接成功的插件才会被注册,连接失败的插件不会被注册
|
|
||||||
// 异步无限重连,只有连接成功了才注册
|
|
||||||
func registerPlugin(msgType messageType, factory configFactory) error {
|
|
||||||
if factory == nil {
|
|
||||||
return fmt.Errorf("factory cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建实例
|
|
||||||
instance := factory()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// 开启异步连接,无限重连直到成功
|
|
||||||
go func() {
|
|
||||||
retryInterval := 2 * time.Second
|
|
||||||
maxInterval := 30 * time.Second
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
g.Log().Errorf(ctx, "❌ [%s] 注册被取消", msgType)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// 尝试连接(使用Reconnect方法)
|
|
||||||
if err := instance.reconnect(ctx); err == nil {
|
|
||||||
// 连接成功,注册插件
|
|
||||||
if err := defaultPluginManager.register(msgType, instance); err != nil {
|
|
||||||
g.Log().Errorf(ctx, "❌ [%s] 注册插件失败: %v", msgType, err)
|
|
||||||
instance.close(ctx)
|
|
||||||
} else {
|
|
||||||
g.Log().Infof(ctx, "✅ [%s] 插件注册成功", msgType)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接失败,记录日志并等待重试
|
|
||||||
g.Log().Warningf(ctx, "⚠️ [%s] 连接失败,%v 后重试...", msgType, retryInterval)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(retryInterval):
|
|
||||||
// 增加重试间隔,但不超过最大值
|
|
||||||
retryInterval *= 2
|
|
||||||
if retryInterval > maxInterval {
|
|
||||||
retryInterval = maxInterval
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
g.Log().Errorf(ctx, "❌ [%s] 注册被取消", msgType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// register 注册插件(内部方法)
|
// register 注册插件(内部方法)
|
||||||
func (m *pluginManager) register(msgType messageType, instance messageUtil) error {
|
func (m *pluginManager) register(msgType messageType, instance messageUtil) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -107,27 +49,66 @@ func (m *pluginManager) register(msgType messageType, instance messageUtil) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMsgPlugin 获取消息队列插件
|
// RegisterPlugin 注册消息队列插件
|
||||||
func GetMsgPlugin(msgType messageType) (messageUtil, error) {
|
// 所有插件必须通过此方法注册,自动进行连接检测
|
||||||
defaultPluginManager.mu.RLock()
|
// 只有连接成功的插件才会被注册,连接失败的插件不会被注册
|
||||||
instance, ok := defaultPluginManager.instances[msgType]
|
// 异步无限重连,只有连接成功了才注册
|
||||||
defaultPluginManager.mu.RUnlock()
|
// name: 数据源名称,用于标识不同的连接实例
|
||||||
|
func RegisterPlugin(ctx context.Context, name string, msgType messageType, factory configFactory) error {
|
||||||
if !ok {
|
if factory == nil {
|
||||||
return nil, fmt.Errorf("unsupported message type: %s", msgType)
|
g.Log().Errorf(ctx, "❌ factory cannot be nil")
|
||||||
|
return fmt.Errorf("factory cannot be nil")
|
||||||
}
|
}
|
||||||
|
// 开启异步连接,无限重试直到成功
|
||||||
return instance, nil
|
go func() {
|
||||||
|
// 创建实例
|
||||||
|
instance := factory()
|
||||||
|
// 创建通知 channel
|
||||||
|
pluginKey := fmt.Sprintf("%s-%s", msgType, name)
|
||||||
|
if !instance.Ping(ctx) {
|
||||||
|
// 使用统一的重连函数
|
||||||
|
if err := commonConnect(ctx, msgType, name, func(ctx context.Context) error {
|
||||||
|
return instance.Connect(ctx)
|
||||||
|
}, func(ctx context.Context) error {
|
||||||
|
return instance.Close(ctx)
|
||||||
|
}); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", msgType, name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 连接成功,注册插件
|
||||||
|
defaultPluginManager.mu.Lock()
|
||||||
|
defaultPluginManager.instances[messageType(pluginKey)] = instance
|
||||||
|
defaultPluginManager.mu.Unlock()
|
||||||
|
g.Log().Infof(ctx, "✅ [%s][%s] 插件注册成功", msgType, name)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSupportedTypes 获取所有已注册的插件类型
|
// GetMsgPlugin 获取消息队列插件(默认数据源),如果未注册则等待
|
||||||
func GetSupportedTypes() []messageType {
|
func GetMsgPlugin(ctx context.Context, msgType messageType) (messageUtil, error) {
|
||||||
defaultPluginManager.mu.RLock()
|
return GetMsgPluginWithName(ctx, msgType, "default")
|
||||||
defer defaultPluginManager.mu.RUnlock()
|
}
|
||||||
|
|
||||||
types := make([]messageType, 0, len(defaultPluginManager.instances))
|
// GetMsgPluginWithName 获取指定数据源的消息队列插件,如果未注册则等待直到超时
|
||||||
for t := range defaultPluginManager.instances {
|
func GetMsgPluginWithName(ctx context.Context, msgType messageType, name string) (messageUtil, error) {
|
||||||
types = append(types, t)
|
pluginKey := fmt.Sprintf("%s-%s", msgType, name)
|
||||||
}
|
|
||||||
return types
|
for {
|
||||||
|
defaultPluginManager.mu.RLock()
|
||||||
|
instance, ok := defaultPluginManager.instances[messageType(pluginKey)]
|
||||||
|
defaultPluginManager.mu.RUnlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return instance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未注册,等待一段时间后重试
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, fmt.Errorf("wait for plugin ready canceled: %s with datasource: %s", msgType, name)
|
||||||
|
default:
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NatsPublishMsgConfig struct {
|
type NatsPublishMsgConfig struct {
|
||||||
|
QueueName string
|
||||||
|
Durable bool
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
type NatsPublishDelayMsgConfig struct {
|
||||||
QueueName string
|
QueueName string
|
||||||
Durable bool
|
Durable bool
|
||||||
DelayTime int
|
DelayTime int
|
||||||
@@ -19,9 +24,9 @@ type NatsPublishMsgConfig struct {
|
|||||||
|
|
||||||
type NatsSubscribeMsgConfig struct {
|
type NatsSubscribeMsgConfig struct {
|
||||||
QueueName string
|
QueueName string
|
||||||
|
ConsumerName string
|
||||||
Durable bool
|
Durable bool
|
||||||
DelayTime int
|
DelayTime int
|
||||||
ConsumerName string
|
|
||||||
AutoAck bool
|
AutoAck bool
|
||||||
PrefetchCount int
|
PrefetchCount int
|
||||||
HandleFunc func(ctx context.Context, message map[string]interface{}) error
|
HandleFunc func(ctx context.Context, message map[string]interface{}) error
|
||||||
@@ -31,32 +36,38 @@ func (*NatsPublishMsgConfig) GetPublishMsgType() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*NatsPublishDelayMsgConfig) GetPublishDelayMsgType() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (*NatsSubscribeMsgConfig) GetSubscribeMsgType() {
|
func (*NatsSubscribeMsgConfig) GetSubscribeMsgType() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type natsMsg struct {
|
||||||
|
name string // 数据源名称
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// 注册 Nats 插件,必须使用 RegisterPlugin 确保连接检测
|
// 注册 Nats 插件(默认数据源)
|
||||||
registerPlugin(MessageNATS, func() messageUtil {
|
RegisterPlugin(context.Background(), "default", MessageNATS, func() messageUtil {
|
||||||
return &natsMsg{}
|
return &natsMsg{name: "default"}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type natsMsg struct{}
|
// Connect 连接 NATS
|
||||||
|
func (c *natsMsg) Connect(ctx context.Context) error {
|
||||||
// Ping 检测 NATS 连接状态
|
return natsConnect(ctx, c.name)
|
||||||
func (c *natsMsg) ping(_ context.Context) bool {
|
|
||||||
return natsPing()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconnect 重连 NATS
|
// Ping 检测 NATS 连接状态
|
||||||
func (c *natsMsg) reconnect(ctx context.Context) error {
|
func (c *natsMsg) Ping(ctx context.Context) bool {
|
||||||
return natsReconnect(ctx)
|
return natsPing(ctx, c.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭 NATS 连接
|
// Close 关闭 NATS 连接
|
||||||
func (c *natsMsg) close(ctx context.Context) error {
|
func (c *natsMsg) Close(ctx context.Context) error {
|
||||||
return natsClose(ctx)
|
return natsClose(ctx, c.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish 发布消息
|
// Publish 发布消息
|
||||||
@@ -71,13 +82,31 @@ func (c *natsMsg) Publish(ctx context.Context, msgConfig messagePublishConfig) e
|
|||||||
if g.IsEmpty(cfg.Data) {
|
if g.IsEmpty(cfg.Data) {
|
||||||
return fmt.Errorf("必须提供数据")
|
return fmt.Errorf("必须提供数据")
|
||||||
}
|
}
|
||||||
|
return c.createPublish(ctx, cfg.QueueName, cfg.Durable, 0, cfg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishDelay 发布延迟消息
|
||||||
|
func (c *natsMsg) PublishDelay(ctx context.Context, msgConfig messagePublishDelayConfig) error {
|
||||||
|
cfg, ok := msgConfig.(*NatsPublishDelayMsgConfig)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("无效的 NATS 配置类型")
|
||||||
|
}
|
||||||
|
if g.IsEmpty(cfg.QueueName) {
|
||||||
|
return fmt.Errorf("必须提供队列名称")
|
||||||
|
}
|
||||||
|
if g.IsEmpty(cfg.DelayTime) {
|
||||||
|
return fmt.Errorf("延迟时间必须大于 0")
|
||||||
|
}
|
||||||
|
if g.IsEmpty(cfg.Data) {
|
||||||
|
return fmt.Errorf("必须提供数据")
|
||||||
|
}
|
||||||
return c.createPublish(ctx, cfg.QueueName, cfg.Durable, cfg.DelayTime, cfg.Data)
|
return c.createPublish(ctx, cfg.QueueName, cfg.Durable, cfg.DelayTime, cfg.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish 发布消息
|
// Publish 发布消息
|
||||||
func (c *natsMsg) createPublish(ctx context.Context, subject string, durable bool, delayTime int, data any) error {
|
func (c *natsMsg) createPublish(ctx context.Context, subject string, durable bool, delayTime int, data any) error {
|
||||||
delayMsg := delayTime > 0
|
delayMsg := delayTime > 0
|
||||||
if err := c.createStreamGroupInternal(ctx, subject, durable, delayMsg); err != nil {
|
if err := c.createStream(ctx, subject, durable, delayMsg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
payload, err := json.Marshal(data)
|
payload, err := json.Marshal(data)
|
||||||
@@ -85,96 +114,30 @@ func (c *natsMsg) createPublish(ctx context.Context, subject string, durable boo
|
|||||||
return fmt.Errorf("序列化数据失败: %w", err)
|
return fmt.Errorf("序列化数据失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := &nats.Msg{
|
m := nats.NewMsg(subject)
|
||||||
Subject: subject,
|
m.Data = payload // 所有消息都需要设置数据
|
||||||
Data: payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
if delayMsg {
|
if delayMsg {
|
||||||
// 计算目标投递时间
|
// 使用 @at 指定具体延迟时间,而不是 @every 重复执行
|
||||||
targetTime := time.Now().Add(time.Duration(delayTime) * time.Second)
|
futureTime := time.Now().Add(time.Duration(delayTime) * time.Second).Format(time.RFC3339Nano)
|
||||||
delayNs := time.Until(targetTime).Nanoseconds()
|
m.Header.Set("Nats-Schedule", fmt.Sprintf("@at %s", futureTime))
|
||||||
if delayNs < 0 {
|
m.Subject = subject + ".schedule"
|
||||||
delayNs = 0
|
m.Header.Set("Nats-Schedule-Target", subject)
|
||||||
}
|
g.Log().Infof(ctx, "📅 NATS 延迟消息配置: DelayTime=%ds, Schedule=@at %s, Header=%s", delayTime, futureTime, m.Header)
|
||||||
|
|
||||||
g.Log().Infof(ctx, "📅 NATS 延迟消息配置: DelayTime=%d秒, TargetTime=%v, DelayNs=%d纳秒(%.2f秒)",
|
|
||||||
delayTime, targetTime.Format("2006-01-02 15:04:05"), delayNs, float64(delayNs)/float64(time.Second.Nanoseconds()))
|
|
||||||
|
|
||||||
// NATS JetStream 延迟消息使用 Nats-Msg-Delay Header(纳秒数)
|
|
||||||
msg.Header = nats.Header{
|
|
||||||
"Nats-Msg-Delay": []string{fmt.Sprintf("%d", delayNs)},
|
|
||||||
}
|
|
||||||
g.Log().Infof(ctx, "📅 NATS 延迟消息 Header: %v", msg.Header)
|
|
||||||
|
|
||||||
// 获取 Stream 配置验证
|
|
||||||
streamName, _ := getStreamInfo(durable, delayMsg)
|
|
||||||
stream, err := js.Stream(ctx, streamName)
|
|
||||||
if err == nil {
|
|
||||||
info, _ := stream.Info(ctx)
|
|
||||||
g.Log().Infof(ctx, "📅 Stream 配置: AllowMsgSchedules=%v, Storage=%v",
|
|
||||||
info.Config.AllowMsgSchedules, info.Config.Storage)
|
|
||||||
if !info.Config.AllowMsgSchedules {
|
|
||||||
g.Log().Errorf(ctx, "❌ Stream 不支持延迟消息!AllowMsgSchedules=false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发布消息到 JetStream
|
// 发布消息到 JetStream
|
||||||
ack, err := js.PublishMsg(ctx, msg)
|
js := getNatsJS(c.name)
|
||||||
|
if js == nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ NATS [%s] JetStream 不存在", c.name)
|
||||||
|
return fmt.Errorf("NATS JetStream 不存在")
|
||||||
|
}
|
||||||
|
ack, err := js.PublishMsg(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Errorf(ctx, "❌ NATS 发布消息失败: err=%v", err)
|
g.Log().Errorf(ctx, "❌ NATS 发布消息失败: err=%v, Subject=%s", err, m.Subject)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
g.Log().Infof(ctx, "✅ NATS 发布消息成功: Stream=%v, StreamSeq=%d", ack.Stream, ack.Sequence)
|
||||||
g.Log().Infof(ctx, "✅ NATS 发布消息成功: StreamSeq=%d, Domain=%s", ack.Sequence, ack.Domain)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createStreamGroup 内部创建消费组
|
|
||||||
func (c *natsMsg) createStreamGroupInternal(ctx context.Context, subject string, durable, delayMsg bool) error {
|
|
||||||
streamName, storage := getStreamInfo(durable, delayMsg)
|
|
||||||
|
|
||||||
// 先检查 Stream 是否存在
|
|
||||||
stream, err := js.Stream(ctx, streamName)
|
|
||||||
if err == nil {
|
|
||||||
// Stream 已存在,检查配置是否匹配
|
|
||||||
info, _ := stream.Info(ctx)
|
|
||||||
if info.Config.AllowMsgSchedules != delayMsg || info.Config.Storage != storage {
|
|
||||||
g.Log().Infof(ctx, "🔄 Stream 配置不匹配,正在重新创建: stream=%s, 当前AllowMsgSchedules=%v, 需要%v",
|
|
||||||
streamName, info.Config.AllowMsgSchedules, delayMsg)
|
|
||||||
// 删除旧 Stream
|
|
||||||
if err := js.DeleteStream(ctx, streamName); err != nil {
|
|
||||||
g.Log().Warningf(ctx, "删除旧 Stream 失败: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
g.Log().Infof(ctx, "✅ Stream 已存在且配置正确: stream=%s", streamName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建流配置
|
|
||||||
jsConfig := jetstream.StreamConfig{
|
|
||||||
Name: streamName,
|
|
||||||
Subjects: []string{subject},
|
|
||||||
AllowMsgSchedules: delayMsg, // 延迟消息核心开关
|
|
||||||
Storage: storage,
|
|
||||||
Discard: jetstream.DiscardOld, // 达到上限删除旧消息
|
|
||||||
}
|
|
||||||
|
|
||||||
stream, err = js.CreateStream(ctx, jsConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("创建任务流失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 Stream 信息验证配置
|
|
||||||
info, err := stream.Info(ctx)
|
|
||||||
if err == nil {
|
|
||||||
g.Log().Infof(ctx, "✅ NATS 队列初始化成功: stream=%s, AllowMsgSchedules=%v, Storage=%v",
|
|
||||||
streamName, info.Config.AllowMsgSchedules, info.Config.Storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Log().Infof(ctx, "✅ NATS 队列初始化成功: stream=%s", streamName)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,110 +159,215 @@ func (c *natsMsg) Subscribe(ctx context.Context, msgConfig messageSubscribeConfi
|
|||||||
if g.IsEmpty(cfg.PrefetchCount) {
|
if g.IsEmpty(cfg.PrefetchCount) {
|
||||||
cfg.PrefetchCount = 1
|
cfg.PrefetchCount = 1
|
||||||
}
|
}
|
||||||
return c.createSubscribeInternal(ctx, cfg.QueueName, cfg.ConsumerName, cfg.PrefetchCount, cfg.AutoAck, cfg.Durable, cfg.DelayTime, cfg.HandleFunc)
|
return c.createSubscribe(ctx, cfg.QueueName, cfg.ConsumerName, cfg.PrefetchCount, cfg.DelayTime, cfg.AutoAck, cfg.Durable, cfg.HandleFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createSubscribe 内部订阅消息
|
// createSubscribe 内部订阅消息
|
||||||
func (c *natsMsg) createSubscribeInternal(ctx context.Context, subject, consumerName string, prefetchCount int, autoAck, durable bool, delayTime int, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
func (c *natsMsg) createSubscribe(ctx context.Context, subject, consumerName string, prefetchCount, delayTime int, autoAck, durable bool, handler func(ctx context.Context, message map[string]any) error) error {
|
||||||
g.Log().Infof(ctx, "🔔 NATS 开始订阅: QueueName=%s, ConsumerName=%s", subject, consumerName)
|
g.Log().Infof(ctx, "🔔 NATS 开始订阅: QueueName=%s, ConsumerName=%s", subject, consumerName)
|
||||||
delayMsg := delayTime > 0
|
// 创建推送订阅的回调函数
|
||||||
streamName, _ := getStreamInfo(durable, delayMsg)
|
msgHandler := func(msg *nats.Msg) {
|
||||||
|
|
||||||
// 确保 Stream 存在,如果不存在则创建
|
|
||||||
if err := c.createStreamGroupInternal(ctx, subject, durable, delayMsg); err != nil {
|
|
||||||
g.Log().Errorf(ctx, "创建 Stream 失败: %v", err)
|
|
||||||
return fmt.Errorf("创建 Stream 失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream 不存在,创建新的
|
|
||||||
ackPolicy := jetstream.AckExplicitPolicy
|
|
||||||
if autoAck {
|
|
||||||
ackPolicy = jetstream.AckNonePolicy
|
|
||||||
}
|
|
||||||
jsConfig := jetstream.ConsumerConfig{
|
|
||||||
Name: consumerName,
|
|
||||||
Durable: consumerName,
|
|
||||||
FilterSubject: subject,
|
|
||||||
AckPolicy: ackPolicy,
|
|
||||||
MaxDeliver: 3,
|
|
||||||
MaxAckPending: prefetchCount,
|
|
||||||
}
|
|
||||||
// 创建新消费者
|
|
||||||
consumer, err := js.CreateOrUpdateConsumer(ctx, streamName, jsConfig)
|
|
||||||
if err != nil {
|
|
||||||
g.Log().Errorf(ctx, "创建消费者失败: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取消费者信息验证
|
|
||||||
if cInfo, err := consumer.Info(ctx); err == nil {
|
|
||||||
g.Log().Infof(ctx, "🔔 消费者创建成功: %s, AckPolicy=%v, MaxAckPending=%d",
|
|
||||||
cInfo.Name, cInfo.Config.AckPolicy, cInfo.Config.MaxAckPending)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建消息处理函数
|
|
||||||
msgHandler := func(msg jetstream.Msg) {
|
|
||||||
// 记录消息接收时间
|
|
||||||
now := time.Now()
|
|
||||||
meta, err := msg.Metadata()
|
|
||||||
if err == nil {
|
|
||||||
g.Log().Infof(ctx, "📨 收到消息: StreamSeq=%d, Published=%v, Received=%v, 距离发布=%.2f秒",
|
|
||||||
meta.Sequence.Stream,
|
|
||||||
meta.Timestamp.Format("2006-01-02 15:04:05"),
|
|
||||||
now.Format("2006-01-02 15:04:05"),
|
|
||||||
now.Sub(meta.Timestamp).Seconds())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析消息
|
|
||||||
var data map[string]any
|
var data map[string]any
|
||||||
if err := json.Unmarshal(msg.Data(), &data); err != nil {
|
if err := json.Unmarshal(msg.Data, &data); err != nil {
|
||||||
g.Log().Errorf(ctx, "解析消息失败: %v", err)
|
g.Log().Errorf(ctx, "❌ 解析消息失败: %v", err)
|
||||||
if err := msg.Nak(); err != nil {
|
|
||||||
g.Log().Errorf(ctx, "Nak 失败: %v", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
g.Log().Infof(ctx, "📨 收到消息: Subject=%s, Data=%v", msg.Subject, data)
|
||||||
|
|
||||||
// 处理业务逻辑
|
// 处理业务逻辑
|
||||||
if err := handler(ctx, data); err != nil {
|
if err := handler(ctx, data); err != nil {
|
||||||
g.Log().Errorf(ctx, "处理消息失败: %v", err)
|
g.Log().Errorf(ctx, "❌ 处理消息失败: %v", err)
|
||||||
if err := msg.Nak(); err != nil {
|
if !autoAck {
|
||||||
g.Log().Errorf(ctx, "Nak 失败: %v", err)
|
if err := msg.Nak(); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Nak 失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
g.Log().Infof(ctx, "✅ 处理消息成功")
|
||||||
|
}
|
||||||
|
if err := msg.Ack(); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ Ack 失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delayMsg := delayTime > 0
|
||||||
|
// 创建流
|
||||||
|
if err := c.createStream(ctx, subject, durable, delayMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 获取 JetStream 上下文
|
||||||
|
js := getNatsJS(c.name)
|
||||||
|
if js == nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ NATS [%s] JetStream 不存在", c.name)
|
||||||
|
return fmt.Errorf("NATS JetStream 不存在")
|
||||||
|
}
|
||||||
|
// 创建推送订阅
|
||||||
|
var sub *nats.Subscription
|
||||||
|
var err error
|
||||||
|
// 配置订阅选项 - 使用 DeliverSubject 创建 Push Consumer
|
||||||
|
subOpts := []nats.SubOpt{
|
||||||
|
nats.Durable(consumerName),
|
||||||
|
nats.MaxAckPending(prefetchCount),
|
||||||
|
nats.DeliverSubject(consumerName),
|
||||||
|
}
|
||||||
|
if !autoAck {
|
||||||
|
subOpts = append(subOpts, nats.ManualAck())
|
||||||
|
}
|
||||||
|
// 使用 Subscribe 创建推送订阅
|
||||||
|
sub, err = js.Subscribe(subject, msgHandler, subOpts...)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "创建推送订阅失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g.Log().Infof(ctx, "✅ NATS 推送订阅成功: Consumer=%s", consumerName)
|
||||||
|
// 启动后台 goroutine 监听上下文取消,用于清理订阅
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
g.Log().Infof(ctx, "订阅上下文取消,取消订阅")
|
||||||
|
if err := sub.Unsubscribe(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.Log().Infof(ctx, "处理消息成功")
|
}()
|
||||||
if !autoAck {
|
|
||||||
if err := msg.Ack(); err != nil {
|
|
||||||
g.Log().Errorf(ctx, "Ack 失败: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始消费
|
|
||||||
_, err = consumer.Consume(msgHandler)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("开始消费失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Log().Infof(ctx, "✅ NATS 订阅成功")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreamInfo(durable, delayMsg bool) (string, jetstream.StorageType) {
|
// createStream 内部创建消费组
|
||||||
|
func (c *natsMsg) createStream(ctx context.Context, subject string, durable, delayMsg bool) error {
|
||||||
|
streamName, storage := getStreamInfo(durable, delayMsg)
|
||||||
|
// 构建流配置
|
||||||
|
// 如果是延迟消息,需要包含两个 subjects:
|
||||||
|
// 1. subject.schedule - 用于发送调度消息
|
||||||
|
// 2. subject - 用于实际投递目标
|
||||||
|
subjects := []string{subject}
|
||||||
|
if delayMsg {
|
||||||
|
subjects = []string{subject, subject + ".schedule"}
|
||||||
|
}
|
||||||
|
jsConfig := &StreamConfig{
|
||||||
|
Name: streamName,
|
||||||
|
Subjects: subjects,
|
||||||
|
AllowMsgSchedules: delayMsg, // 延迟消息核心开关
|
||||||
|
Storage: storage,
|
||||||
|
Discard: DiscardNew, // 达到上限删除旧消息
|
||||||
|
}
|
||||||
|
nc := getNatsConn(c.name)
|
||||||
|
if !c.Ping(ctx) {
|
||||||
|
// 使用统一的重连函数
|
||||||
|
if err := commonConnect(ctx, MessageNATS, c.name, func(ctx context.Context) error {
|
||||||
|
return c.Connect(ctx)
|
||||||
|
}, func(ctx context.Context) error {
|
||||||
|
return c.Close(ctx)
|
||||||
|
}); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", MessageNATS, c.name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nc == nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ NATS [%s] 连接不存在", c.name)
|
||||||
|
return fmt.Errorf("NATS 连接不存在")
|
||||||
|
}
|
||||||
|
err := jsStreamCreate(nc, jsConfig)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ 创建 Stream 失败: err=%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g.Log().Infof(ctx, "✅ 创建 Stream 成功: stream=%s, subjects=%v, allowSchedules=%v", streamName, subjects, delayMsg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStreamInfo(durable, delayMsg bool) (string, StorageType) {
|
||||||
// Stream 不存在,创建新的
|
// Stream 不存在,创建新的
|
||||||
streamName := "ordinary_msg_memory"
|
streamName := "ordinary_msg_memory"
|
||||||
storage := jetstream.MemoryStorage
|
storage := MemoryStorage
|
||||||
|
|
||||||
// 延迟消息必须使用 FileStorage(NATS 官方要求)
|
// 延迟消息必须使用 FileStorage(NATS 官方要求)
|
||||||
if delayMsg {
|
if delayMsg {
|
||||||
streamName = "delay_msg_file"
|
if durable {
|
||||||
storage = jetstream.FileStorage
|
streamName = "delay_msg_file"
|
||||||
|
storage = FileStorage
|
||||||
|
} else {
|
||||||
|
streamName = "delay_msg_memory"
|
||||||
|
storage = MemoryStorage
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if durable {
|
if durable {
|
||||||
streamName = "ordinary_msg_file"
|
streamName = "ordinary_msg_file"
|
||||||
storage = jetstream.FileStorage
|
storage = FileStorage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return streamName, storage
|
return streamName, storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// JSApiStreamCreateT is the endpoint to create new streams.
|
||||||
|
// Will return JSON response.
|
||||||
|
JSApiStreamCreateT = "$JS.API.STREAM.CREATE.%s"
|
||||||
|
|
||||||
|
// JSApiStreamUpdateT is the endpoint to update existing streams.
|
||||||
|
// Will return JSON response.
|
||||||
|
JSApiStreamUpdateT = "$JS.API.STREAM.UPDATE.%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// jsStreamCreate is for sending a stream create for fields that nats.go does not know about yet.
|
||||||
|
func jsStreamCreate(nc *nats.Conn, cfg *StreamConfig) error {
|
||||||
|
j, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := nc.Request(fmt.Sprintf(JSApiStreamCreateT, cfg.Name), j, time.Second*3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 API 响应中的错误
|
||||||
|
var resp struct {
|
||||||
|
Error *struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
ErrCode int `json:"err_code"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
} `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(msg.Data, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Error != nil {
|
||||||
|
// 如果 Stream 已存在,尝试更新
|
||||||
|
if resp.Error.ErrCode == 10058 { // JSStreamNameExistErr
|
||||||
|
return jsStreamUpdate(nc, cfg)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("JS API error: %s", resp.Error.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsStreamUpdate is for sending a stream create for fields that nats.go does not know about yet.
|
||||||
|
func jsStreamUpdate(nc *nats.Conn, cfg *StreamConfig) error {
|
||||||
|
j, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg, err := nc.Request(fmt.Sprintf(JSApiStreamUpdateT, cfg.Name), j, time.Second*3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 API 响应中的错误
|
||||||
|
var resp struct {
|
||||||
|
Error *struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
ErrCode int `json:"err_code"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
} `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(msg.Data, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Error != nil {
|
||||||
|
return fmt.Errorf("JS API error: %s", resp.Error.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ var (
|
|||||||
traceCancelMu sync.RWMutex
|
traceCancelMu sync.RWMutex
|
||||||
// 取消主题前缀
|
// 取消主题前缀
|
||||||
cancelSubjectPrefix = "ctx.cancel.otel."
|
cancelSubjectPrefix = "ctx.cancel.otel."
|
||||||
|
|
||||||
|
// RPC 使用的默认数据源名称
|
||||||
|
rpcDefaultDatasource = "default"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rpcHandler RPC 处理函数类型
|
// rpcHandler RPC 处理函数类型
|
||||||
@@ -42,7 +45,7 @@ type rpcHandler func(ctx context.Context, req []byte) (any, error)
|
|||||||
// serviceName: 服务名称,调用方通过此名称调用服务
|
// serviceName: 服务名称,调用方通过此名称调用服务
|
||||||
// handler: 服务处理函数,接收请求并返回响应
|
// handler: 服务处理函数,接收请求并返回响应
|
||||||
func registerRPCService(serviceName string, handler rpcHandler) (err error) {
|
func registerRPCService(serviceName string, handler rpcHandler) (err error) {
|
||||||
if !natsPing() {
|
if !natsPing(context.Background(), rpcDefaultDatasource) {
|
||||||
return fmt.Errorf("NATS 未连接")
|
return fmt.Errorf("NATS 未连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +66,11 @@ func registerRPCService(serviceName string, handler rpcHandler) (err error) {
|
|||||||
rpcServicesMu.Unlock()
|
rpcServicesMu.Unlock()
|
||||||
|
|
||||||
// 订阅服务主题
|
// 订阅服务主题
|
||||||
|
nc := getNatsConn(rpcDefaultDatasource)
|
||||||
|
if nc == nil {
|
||||||
|
return fmt.Errorf("NATS 连接不存在")
|
||||||
|
}
|
||||||
|
|
||||||
subject := fmt.Sprintf("rpc.%s", serviceName)
|
subject := fmt.Sprintf("rpc.%s", serviceName)
|
||||||
sub, err := nc.Subscribe(subject, func(msg *nats.Msg) {
|
sub, err := nc.Subscribe(subject, func(msg *nats.Msg) {
|
||||||
// 执行处理函数
|
// 执行处理函数
|
||||||
@@ -84,7 +92,7 @@ func registerRPCService(serviceName string, handler rpcHandler) (err error) {
|
|||||||
// queueName: 队列组名,同一队列组的实例共享请求
|
// queueName: 队列组名,同一队列组的实例共享请求
|
||||||
// handler: 服务处理函数
|
// handler: 服务处理函数
|
||||||
func registerQueueRPCService(serviceName, queueName string, handler rpcHandler) (err error) {
|
func registerQueueRPCService(serviceName, queueName string, handler rpcHandler) (err error) {
|
||||||
if !natsPing() {
|
if !natsPing(context.Background(), rpcDefaultDatasource) {
|
||||||
return fmt.Errorf("NATS 未连接")
|
return fmt.Errorf("NATS 未连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +119,11 @@ func registerQueueRPCService(serviceName, queueName string, handler rpcHandler)
|
|||||||
queueRPCMu.Unlock()
|
queueRPCMu.Unlock()
|
||||||
|
|
||||||
// 订阅服务主题(队列模式)
|
// 订阅服务主题(队列模式)
|
||||||
|
nc := getNatsConn(rpcDefaultDatasource)
|
||||||
|
if nc == nil {
|
||||||
|
return fmt.Errorf("NATS 连接不存在")
|
||||||
|
}
|
||||||
|
|
||||||
subject := fmt.Sprintf("rpc.%s", serviceName)
|
subject := fmt.Sprintf("rpc.%s", serviceName)
|
||||||
sub, err := nc.QueueSubscribe(subject, queueName, func(msg *nats.Msg) {
|
sub, err := nc.QueueSubscribe(subject, queueName, func(msg *nats.Msg) {
|
||||||
// 执行处理函数
|
// 执行处理函数
|
||||||
@@ -209,7 +222,7 @@ func createCancelContext(ctx context.Context, traceID string) context.Context {
|
|||||||
//
|
//
|
||||||
// sub, err := nats.SetupCancelListener(ctx)
|
// sub, err := nats.SetupCancelListener(ctx)
|
||||||
func setupCancelListener(ctx context.Context) (*nats.Subscription, error) {
|
func setupCancelListener(ctx context.Context) (*nats.Subscription, error) {
|
||||||
if !natsPing() {
|
if !natsPing(ctx, rpcDefaultDatasource) {
|
||||||
return nil, fmt.Errorf("NATS 未连接")
|
return nil, fmt.Errorf("NATS 未连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +232,11 @@ func setupCancelListener(ctx context.Context) (*nats.Subscription, error) {
|
|||||||
|
|
||||||
// 修复问题3:订阅取消主题,格式: ctx.cancel.otel.*
|
// 修复问题3:订阅取消主题,格式: ctx.cancel.otel.*
|
||||||
// 使用 * 通配符而不是 >,因为 TraceID 是最后一部分
|
// 使用 * 通配符而不是 >,因为 TraceID 是最后一部分
|
||||||
|
nc := getNatsConn(rpcDefaultDatasource)
|
||||||
|
if nc == nil {
|
||||||
|
return nil, fmt.Errorf("NATS 连接不存在")
|
||||||
|
}
|
||||||
|
|
||||||
cancelSubject := cancelSubjectPrefix + "*"
|
cancelSubject := cancelSubjectPrefix + "*"
|
||||||
sub, err := nc.Subscribe(cancelSubject, func(msg *nats.Msg) {
|
sub, err := nc.Subscribe(cancelSubject, func(msg *nats.Msg) {
|
||||||
// 从主题中解析 TraceID (去除前缀)
|
// 从主题中解析 TraceID (去除前缀)
|
||||||
@@ -261,7 +279,7 @@ func setupCancelListener(ctx context.Context) (*nats.Subscription, error) {
|
|||||||
//
|
//
|
||||||
// err := nats.publishCancel(ctx, traceID)
|
// err := nats.publishCancel(ctx, traceID)
|
||||||
func publishCancel(ctx context.Context, traceID string) error {
|
func publishCancel(ctx context.Context, traceID string) error {
|
||||||
if !natsPing() {
|
if !natsPing(ctx, rpcDefaultDatasource) {
|
||||||
return fmt.Errorf("NATS 未连接")
|
return fmt.Errorf("NATS 未连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +287,11 @@ func publishCancel(ctx context.Context, traceID string) error {
|
|||||||
return fmt.Errorf("TraceID 不能为空")
|
return fmt.Errorf("TraceID 不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nc := getNatsConn(rpcDefaultDatasource)
|
||||||
|
if nc == nil {
|
||||||
|
return fmt.Errorf("NATS 连接不存在")
|
||||||
|
}
|
||||||
|
|
||||||
cancelSubject := cancelSubjectPrefix + traceID
|
cancelSubject := cancelSubjectPrefix + traceID
|
||||||
err := nc.Publish(cancelSubject, nil)
|
err := nc.Publish(cancelSubject, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -303,7 +326,7 @@ func cleanupTraceCancel(traceID string) {
|
|||||||
// req: 请求数据
|
// req: 请求数据
|
||||||
// 返回: 响应数据(任意类型)和错误
|
// 返回: 响应数据(任意类型)和错误
|
||||||
func CallRPC(ctx context.Context, serviceName string, req any, resp any) (err error) {
|
func CallRPC(ctx context.Context, serviceName string, req any, resp any) (err error) {
|
||||||
if !natsPing() {
|
if !natsPing(ctx, rpcDefaultDatasource) {
|
||||||
return fmt.Errorf("NATS 未连接")
|
return fmt.Errorf("NATS 未连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +429,11 @@ func CallRPC(ctx context.Context, serviceName string, req any, resp any) (err er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
|
nc := getNatsConn(rpcDefaultDatasource)
|
||||||
|
if nc == nil {
|
||||||
|
return fmt.Errorf("NATS 连接不存在")
|
||||||
|
}
|
||||||
|
|
||||||
responseMsg, err := nc.RequestMsgWithContext(ctx, msg)
|
responseMsg, err := nc.RequestMsgWithContext(ctx, msg)
|
||||||
|
|
||||||
// 关闭 done channel,通知 goroutine 退出
|
// 关闭 done channel,通知 goroutine 退出
|
||||||
@@ -475,7 +503,7 @@ func WithExcludeMethods(methods ...string) registerServiceOption {
|
|||||||
// }, WithQueueGroup("order-group"))
|
// }, WithQueueGroup("order-group"))
|
||||||
func AutoRegisterServices(ctx context.Context, serviceInstances map[string]interface{}, options ...registerServiceOption) error {
|
func AutoRegisterServices(ctx context.Context, serviceInstances map[string]interface{}, options ...registerServiceOption) error {
|
||||||
// 先注册 RPC 服务(如果 NATS 不可用则记录警告但不阻塞启动)
|
// 先注册 RPC 服务(如果 NATS 不可用则记录警告但不阻塞启动)
|
||||||
if !natsPing() {
|
if !natsPing(ctx, rpcDefaultDatasource) {
|
||||||
return fmt.Errorf("NATS 未连接,RPC 服务未注册")
|
return fmt.Errorf("NATS 未连接,RPC 服务未注册")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +540,7 @@ func AutoRegisterServices(ctx context.Context, serviceInstances map[string]inter
|
|||||||
|
|
||||||
// registerService 注册单个服务的所有公开方法(内部函数)
|
// registerService 注册单个服务的所有公开方法(内部函数)
|
||||||
func registerService(service interface{}, serviceNamePrefix string, options ...registerServiceOption) (err error) {
|
func registerService(service interface{}, serviceNamePrefix string, options ...registerServiceOption) (err error) {
|
||||||
if !natsPing() {
|
if !natsPing(context.Background(), rpcDefaultDatasource) {
|
||||||
return fmt.Errorf("NATS 未连接")
|
return fmt.Errorf("NATS 未连接")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RabbitMQPublishMsgConfig struct {
|
type RabbitMQPublishMsgConfig struct {
|
||||||
|
QueueName string
|
||||||
|
Durable bool
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
type RabbitMQPublishDelayMsgConfig struct {
|
||||||
QueueName string
|
QueueName string
|
||||||
Durable bool
|
Durable bool
|
||||||
DelayTime int
|
DelayTime int
|
||||||
@@ -19,8 +25,6 @@ type RabbitMQPublishMsgConfig struct {
|
|||||||
|
|
||||||
type RabbitMQSubscribeMsgConfig struct {
|
type RabbitMQSubscribeMsgConfig struct {
|
||||||
QueueName string
|
QueueName string
|
||||||
Durable bool
|
|
||||||
DelayTime int
|
|
||||||
ConsumerName string
|
ConsumerName string
|
||||||
AutoAck bool
|
AutoAck bool
|
||||||
PrefetchCount int
|
PrefetchCount int
|
||||||
@@ -31,32 +35,36 @@ func (*RabbitMQPublishMsgConfig) GetPublishMsgType() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*RabbitMQPublishDelayMsgConfig) GetPublishDelayMsgType() {}
|
||||||
|
|
||||||
func (*RabbitMQSubscribeMsgConfig) GetSubscribeMsgType() {
|
func (*RabbitMQSubscribeMsgConfig) GetSubscribeMsgType() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
type rabbitMQ struct {
|
||||||
// 注册 RabbitMQ 插件,必须使用 RegisterPlugin 确保连接检测
|
name string // 数据源名称
|
||||||
//registerPlugin(MessageRabbitMQ, func() messageUtil {
|
|
||||||
// return &rabbitMQ{}
|
|
||||||
//})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type rabbitMQ struct{}
|
func init() {
|
||||||
|
// 注册 RabbitMQ 插件(默认数据源)
|
||||||
|
RegisterPlugin(context.Background(), "default", MessageRabbitMQ, func() messageUtil {
|
||||||
|
return &rabbitMQ{name: "default"}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 连接 RabbitMQ
|
||||||
|
func (c *rabbitMQ) Connect(ctx context.Context) error {
|
||||||
|
return rabbitmqConnect(ctx, c.name)
|
||||||
|
}
|
||||||
|
|
||||||
// Ping 检测 RabbitMQ 连接状态
|
// Ping 检测 RabbitMQ 连接状态
|
||||||
func (c *rabbitMQ) ping(ctx context.Context) bool {
|
func (c *rabbitMQ) Ping(ctx context.Context) bool {
|
||||||
return rabbitmqPing()
|
return rabbitmqPing(ctx, c.name)
|
||||||
}
|
|
||||||
|
|
||||||
// Reconnect 重连 RabbitMQ
|
|
||||||
func (c *rabbitMQ) reconnect(ctx context.Context) error {
|
|
||||||
return rabbitmqReconnect(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭 RabbitMQ 连接
|
// Close 关闭 RabbitMQ 连接
|
||||||
func (c *rabbitMQ) close(ctx context.Context) error {
|
func (c *rabbitMQ) Close(ctx context.Context) error {
|
||||||
return rabbitmqClose(ctx)
|
return rabbitmqClose(ctx, c.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish 发布消息
|
// Publish 发布消息
|
||||||
@@ -71,11 +79,43 @@ func (c *rabbitMQ) Publish(ctx context.Context, msgConfig messagePublishConfig)
|
|||||||
if cfg.Data == nil {
|
if cfg.Data == nil {
|
||||||
return fmt.Errorf("数据不能为空")
|
return fmt.Errorf("数据不能为空")
|
||||||
}
|
}
|
||||||
|
return c.publishMessageInternal(ctx, cfg.QueueName, cfg.Durable, 0, cfg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishDelay 发布延迟消息
|
||||||
|
func (c *rabbitMQ) PublishDelay(ctx context.Context, msgConfig messagePublishDelayConfig) error {
|
||||||
|
cfg, ok := msgConfig.(*RabbitMQPublishDelayMsgConfig)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("无效的 RabbitMQ 配置类型")
|
||||||
|
}
|
||||||
|
if g.IsEmpty(cfg.QueueName) {
|
||||||
|
return fmt.Errorf("队列名称不能为空")
|
||||||
|
}
|
||||||
|
if cfg.Data == nil {
|
||||||
|
return fmt.Errorf("数据不能为空")
|
||||||
|
}
|
||||||
return c.publishMessageInternal(ctx, cfg.QueueName, cfg.Durable, cfg.DelayTime, cfg.Data)
|
return c.publishMessageInternal(ctx, cfg.QueueName, cfg.Durable, cfg.DelayTime, cfg.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// publishMessage 发布消息内部实现
|
// publishMessage 发布消息内部实现
|
||||||
func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string, durable bool, delayTime int, data interface{}) error {
|
func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string, durable bool, delayTime int, data interface{}) error {
|
||||||
|
if !c.Ping(ctx) {
|
||||||
|
if err := commonConnect(ctx, MessageRabbitMQ, c.name, func(ctx context.Context) error {
|
||||||
|
return c.Connect(ctx)
|
||||||
|
}, func(ctx context.Context) error {
|
||||||
|
return c.Close(ctx)
|
||||||
|
}); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", MessageRabbitMQ, c.name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := getRabbitMQChannel(c.name)
|
||||||
|
if channel == nil || channel.IsClosed() {
|
||||||
|
g.Log().Errorf(ctx, "❌ RabbitMQ [%s] Channel 不存在或已关闭", c.name)
|
||||||
|
return fmt.Errorf("RabbitMQ Channel 不存在或已关闭")
|
||||||
|
}
|
||||||
|
|
||||||
delayMsg := delayTime > 0
|
delayMsg := delayTime > 0
|
||||||
|
|
||||||
// 1. 决定 Exchange 类型
|
// 1. 决定 Exchange 类型
|
||||||
@@ -86,12 +126,12 @@ func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string,
|
|||||||
if delayMsg {
|
if delayMsg {
|
||||||
exchangeType = "x-delayed-message"
|
exchangeType = "x-delayed-message"
|
||||||
exchangeName = queueName + ".delayed"
|
exchangeName = queueName + ".delayed"
|
||||||
args["x-delayed-type"] = "fanout" // 底层用 topic
|
args["x-delayed-type"] = "fanout"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 声明 Exchange(只声明一次)
|
// 2. 声明 Exchange(使用 exchangeName 而不是 queueName)
|
||||||
if err := channel.ExchangeDeclare(
|
if err := channel.ExchangeDeclare(
|
||||||
queueName, // exchange 交换机名称
|
exchangeName, // 修复:使用正确的交换机名称
|
||||||
exchangeType,
|
exchangeType,
|
||||||
durable,
|
durable,
|
||||||
false, // autoDelete
|
false, // autoDelete
|
||||||
@@ -99,7 +139,8 @@ func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string,
|
|||||||
false, // noWait
|
false, // noWait
|
||||||
args,
|
args,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("声明 Exchange 失败: %w", err)
|
g.Log().Errorf(ctx, "❌ 声明 Exchange 失败: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 声明队列
|
// 3. 声明队列
|
||||||
@@ -111,7 +152,8 @@ func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string,
|
|||||||
false, // noWait
|
false, // noWait
|
||||||
nil, // args
|
nil, // args
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("声明队列失败: %w", err)
|
g.Log().Errorf(ctx, "❌ 声明队列失败: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 绑定队列
|
// 4. 绑定队列
|
||||||
@@ -122,13 +164,15 @@ func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string,
|
|||||||
false, // noWait
|
false, // noWait
|
||||||
nil, // args
|
nil, // args
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("绑定队列失败: %w", err)
|
g.Log().Errorf(ctx, "❌ 绑定队列失败: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 序列化数据
|
// 5. 序列化数据
|
||||||
body, err := json.Marshal(data)
|
body, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("序列化数据失败: %w", err)
|
g.Log().Errorf(ctx, "❌ 序列化数据失败: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
// 6. 发布消息
|
// 6. 发布消息
|
||||||
deliveryMode := amqp.Transient
|
deliveryMode := amqp.Transient
|
||||||
@@ -142,9 +186,9 @@ func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string,
|
|||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
if delayMsg {
|
if delayMsg {
|
||||||
duration := time.Duration(delayTime) * time.Minute
|
duration := delayTime * 1000 // 延迟时间(毫秒)= 秒 * 1000
|
||||||
publishing.Headers = amqp.Table{
|
publishing.Headers = amqp.Table{
|
||||||
"x-delay": duration, // 延迟时间(毫秒)
|
"x-delay": duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = channel.PublishWithContext(
|
err = channel.PublishWithContext(
|
||||||
@@ -154,6 +198,11 @@ func (c *rabbitMQ) publishMessageInternal(ctx context.Context, queueName string,
|
|||||||
false, false,
|
false, false,
|
||||||
publishing,
|
publishing,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ 发布消息失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g.Log().Infof(ctx, "📨 发布消息成功: queueName=%s, data=%v", queueName, data)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,10 +229,28 @@ func (c *rabbitMQ) Subscribe(ctx context.Context, msgConfig messageSubscribeConf
|
|||||||
|
|
||||||
// createSubscribe 内部订阅消息
|
// createSubscribe 内部订阅消息
|
||||||
func (c *rabbitMQ) createSubscribeInternal(ctx context.Context, queueName, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
func (c *rabbitMQ) createSubscribeInternal(ctx context.Context, queueName, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
||||||
g.Log().Infof(ctx, "🔔 RabbitMQ 开始订阅: queueName=%s, consumerName=%s", queueName, consumerName)
|
g.Log().Infof(ctx, "🔔 RabbitMQ [%s] 开始订阅: queueName=%s, consumerName=%s", c.name, queueName, consumerName)
|
||||||
|
|
||||||
|
if !c.Ping(ctx) {
|
||||||
|
if err := commonConnect(ctx, MessageRabbitMQ, c.name, func(ctx context.Context) error {
|
||||||
|
return c.Connect(ctx)
|
||||||
|
}, func(ctx context.Context) error {
|
||||||
|
return c.Close(ctx)
|
||||||
|
}); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", MessageRabbitMQ, c.name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := getRabbitMQChannel(c.name)
|
||||||
|
if channel == nil || channel.IsClosed() {
|
||||||
|
g.Log().Errorf(ctx, "❌ RabbitMQ [%s] Channel 不存在或已关闭", c.name)
|
||||||
|
return fmt.Errorf("RabbitMQ Channel 不存在或已关闭")
|
||||||
|
}
|
||||||
|
|
||||||
if err := channel.Qos(prefetchCount, 0, false); err != nil {
|
if err := channel.Qos(prefetchCount, 0, false); err != nil {
|
||||||
return fmt.Errorf("设置 Qos 失败: %w", err)
|
g.Log().Errorf(ctx, "❌ 设置 Qos 失败: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
g.Log().Infof(ctx, "📊 设置 Prefetch Count: %d", prefetchCount)
|
g.Log().Infof(ctx, "📊 设置 Prefetch Count: %d", prefetchCount)
|
||||||
|
|
||||||
@@ -197,97 +264,48 @@ func (c *rabbitMQ) createSubscribeInternal(ctx context.Context, queueName, consu
|
|||||||
nil, // args
|
nil, // args
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("注册消费者失败: %w", err)
|
g.Log().Errorf(ctx, "❌ 消费消息失败: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
g.Log().Infof(ctx, "👀 开始监听消息")
|
||||||
go func() {
|
for {
|
||||||
defer func() {
|
select {
|
||||||
if r := recover(); r != nil {
|
case <-ctx.Done():
|
||||||
g.Log().Errorf(ctx, "❌ RabbitMQ 消费者 panic: %v", r)
|
// Context 取消,退出
|
||||||
|
g.Log().Infof(ctx, "context cancel 监听消息退出")
|
||||||
|
return nil
|
||||||
|
case m, ok := <-msg:
|
||||||
|
if !ok {
|
||||||
|
// Channel 关闭,退出
|
||||||
|
g.Log().Infof(ctx, "channel close 监听消息退出")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}()
|
g.Log().Infof(ctx, "📨 收到消息: %s", string(m.Body))
|
||||||
|
|
||||||
// 并发控制信号量
|
var data map[string]interface{}
|
||||||
semaphore := make(chan struct{}, 10) // 限制最大并发数为 10
|
if err := json.Unmarshal(m.Body, &data); err != nil {
|
||||||
|
// 如果不是 JSON,直接使用原始内容
|
||||||
for {
|
data = map[string]interface{}{
|
||||||
select {
|
"data": string(m.Body),
|
||||||
case <-ctx.Done():
|
|
||||||
g.Log().Infof(ctx, "🔕 RabbitMQ 消费者停止: queueName=%s, consumerName=%s", queueName, consumerName)
|
|
||||||
return
|
|
||||||
case msg, ok := <-msg:
|
|
||||||
if !ok {
|
|
||||||
g.Log().Warningf(ctx, "⚠️ RabbitMQ 消息通道关闭")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 获取并发控制槽位
|
err := handler(ctx, data)
|
||||||
semaphore <- struct{}{}
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ 消息处理失败: %v", err)
|
||||||
go func(m amqp.Delivery) {
|
// 仅在手动 ACK 模式下拒绝消息
|
||||||
defer func() {
|
if !autoAck {
|
||||||
<-semaphore // 释放槽位
|
// 拒绝消息不再重新入队(避免死循环)
|
||||||
if r := recover(); r != nil {
|
m.Nack(false, false)
|
||||||
g.Log().Errorf(ctx, "❌ 消息处理 panic: %v", r)
|
continue
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
g.Log().Infof(ctx, "✅ 消息处理成功: %v", err)
|
||||||
if err := c.handleMessageWithRetryInternal(ctx, m, handler, autoAck); err != nil {
|
// 仅在手动 ACK 模式下确认消息
|
||||||
g.Log().Errorf(ctx, "❌ 消息处理失败(重试次数耗尽): %v", err)
|
if err := m.Ack(false); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ AUTO ACK 消息失败: %v", err)
|
||||||
// 仅在手动 ACK 模式下拒绝消息
|
} else {
|
||||||
if !autoAck {
|
g.Log().Infof(ctx, "✅ AUTO ACK 消息成功")
|
||||||
// 拒绝消息不再重新入队(避免死循环)
|
|
||||||
m.Nack(false, false)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 仅在手动 ACK 模式下确认消息
|
|
||||||
if autoAck {
|
|
||||||
if err := m.Ack(false); err != nil {
|
|
||||||
g.Log().Errorf(ctx, "❌ ACK 消息失败: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleMessageWithRetry 处理消息(支持重试)
|
|
||||||
func (c *rabbitMQ) handleMessageWithRetryInternal(ctx context.Context, msg amqp.Delivery, handler func(ctx context.Context, message map[string]interface{}) error, autoAck bool) error {
|
|
||||||
var data map[string]interface{}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(msg.Body, &data); err != nil {
|
|
||||||
// 如果不是 JSON,直接使用原始内容
|
|
||||||
data = map[string]interface{}{
|
|
||||||
"data": string(msg.Body),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重试逻辑
|
|
||||||
const maxRetry = 3
|
|
||||||
for attempt := 0; attempt <= maxRetry; attempt++ {
|
|
||||||
if attempt > 0 {
|
|
||||||
g.Log().Infof(ctx, "🔄 消息处理重试 (第%d次)", attempt)
|
|
||||||
// 指数退避
|
|
||||||
time.Sleep(time.Duration(attempt) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := handler(ctx, data)
|
|
||||||
if err == nil {
|
|
||||||
return nil // 成功
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Log().Warningf(ctx, "⚠️ 消息处理失败 (第%d次): %v", attempt+1, err)
|
|
||||||
|
|
||||||
if attempt == maxRetry {
|
|
||||||
return fmt.Errorf("达到最大重试次数 %d: %w", maxRetry, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
73
message/reconnect.go
Normal file
73
message/reconnect.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectFunc 连接函数类型
|
||||||
|
type connectFunc func(ctx context.Context) error
|
||||||
|
|
||||||
|
// closeFunc 关闭函数类型
|
||||||
|
type closeFunc func(ctx context.Context) error
|
||||||
|
|
||||||
|
// reconnectOption 重连选项
|
||||||
|
type reconnectOption struct {
|
||||||
|
maxRetries int // 最大重试次数,0 表示无限重试
|
||||||
|
interval time.Duration // 重试间隔
|
||||||
|
componentType messageType // 组件类型(nats/redis/rabbitmq)
|
||||||
|
componentName string // 组件名称(数据源名称)
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultReconnectOption 默认重连选项
|
||||||
|
func defaultReconnectOption(componentType messageType, componentName string) *reconnectOption {
|
||||||
|
return &reconnectOption{
|
||||||
|
maxRetries: 0, // 无限重试
|
||||||
|
interval: 3 * time.Second,
|
||||||
|
componentType: componentType,
|
||||||
|
componentName: componentName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// commonReconnect 重连函数(NATS、Redis、RabbitMQ 共用)
|
||||||
|
func commonReconnect(ctx context.Context, connectFn connectFunc, closeFn closeFunc, opt *reconnectOption) error {
|
||||||
|
if opt == nil {
|
||||||
|
opt = defaultReconnectOption("unknown", "default")
|
||||||
|
}
|
||||||
|
|
||||||
|
for attempt := 0; opt.maxRetries == 0 || attempt < opt.maxRetries; attempt++ {
|
||||||
|
err := connectFn(ctx)
|
||||||
|
if err == nil {
|
||||||
|
g.Log().Infof(ctx, "✅ 连接成功: type=%s, name=%s, attempt=%d",
|
||||||
|
opt.componentType, opt.componentName, attempt+1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 记录失败日志
|
||||||
|
g.Log().Warningf(ctx, "⚠️ 连接失败: type=%s, name=%s, attempt=%d, err=%v, 重试中...",
|
||||||
|
opt.componentType, opt.componentName, attempt+1, err)
|
||||||
|
// 如果错误信息中包含 "does not exist",则认为是连接失败,不再重试
|
||||||
|
if strings.Contains(err.Error(), "does not exist") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 等待一段时间再重试
|
||||||
|
select {
|
||||||
|
case <-time.After(opt.interval):
|
||||||
|
case <-ctx.Done():
|
||||||
|
if err = closeFn(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("连接失败,已达最大重试次数")
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect 连接函数,直接调用 commonReconnect
|
||||||
|
func commonConnect(ctx context.Context, componentType messageType, name string, connectFn func(ctx context.Context) error, closeFn closeFunc) error {
|
||||||
|
opt := defaultReconnectOption(componentType, name)
|
||||||
|
return commonReconnect(ctx, connectFn, closeFn, opt)
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ package message
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gogf/gf/v2/os/glog"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/glog"
|
|
||||||
"github.com/gogf/gf/v2/util/gconv"
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ type RedisPublishMsgConfig struct {
|
|||||||
Data any
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedisPublishDelayMsgConfig struct {
|
||||||
|
}
|
||||||
|
|
||||||
type RedisSubscribeMsgConfig struct {
|
type RedisSubscribeMsgConfig struct {
|
||||||
QueueName string
|
QueueName string
|
||||||
ConsumerName string
|
ConsumerName string
|
||||||
@@ -28,18 +31,22 @@ func (*RedisPublishMsgConfig) GetPublishMsgType() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*RedisPublishDelayMsgConfig) GetPublishDelayMsgType() {}
|
||||||
|
|
||||||
func (*RedisSubscribeMsgConfig) GetSubscribeMsgType() {
|
func (*RedisSubscribeMsgConfig) GetSubscribeMsgType() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
type redis struct {
|
||||||
// 注册 Redis 插件(连接由 RegisterPlugin 异步处理)
|
name string // 数据源名称
|
||||||
registerPlugin(MessageRedis, func() messageUtil {
|
|
||||||
return &redis{}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type redis struct{}
|
func init() {
|
||||||
|
// 注册 Redis 插件(默认数据源)
|
||||||
|
RegisterPlugin(context.Background(), "default", MessageRedis, func() messageUtil {
|
||||||
|
return &redis{name: "default"}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// RedisStreamMessage Redis Stream 消息结构
|
// RedisStreamMessage Redis Stream 消息结构
|
||||||
type redisStreamMessage struct {
|
type redisStreamMessage struct {
|
||||||
@@ -47,41 +54,19 @@ type redisStreamMessage struct {
|
|||||||
Values map[string]interface{}
|
Values map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping 检测 Redis 连接状态
|
// Connect 连接 Redis
|
||||||
func (c *redis) ping(ctx context.Context) bool {
|
func (c *redis) Connect(ctx context.Context) error {
|
||||||
conn, err := getDefaultDataSource()
|
return redisConnect(ctx, c.name)
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return conn.redisPing(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconnect 重连 Redis
|
// Ping 检测 Redis 连接状态
|
||||||
func (c *redis) reconnect(ctx context.Context) error {
|
func (c *redis) Ping(ctx context.Context) bool {
|
||||||
conn, err := getDefaultDataSource()
|
return redisPing(ctx, c.name)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conn.redisReconnect(ctx); err != nil {
|
|
||||||
return fmt.Errorf("redis重连失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭 Redis 连接
|
// Close 关闭 Redis 连接
|
||||||
func (c *redis) close(ctx context.Context) error {
|
func (c *redis) Close(ctx context.Context) error {
|
||||||
conn, err := getDefaultDataSource()
|
return redisClose(ctx, c.name)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conn.redisClose(ctx); err != nil {
|
|
||||||
return fmt.Errorf("关闭redis连接失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish 发布消息
|
// Publish 发布消息
|
||||||
@@ -96,14 +81,16 @@ func (c *redis) Publish(ctx context.Context, msgConfig messagePublishConfig) err
|
|||||||
if g.IsEmpty(cfg.Data) {
|
if g.IsEmpty(cfg.Data) {
|
||||||
return fmt.Errorf("数据不能为空")
|
return fmt.Errorf("数据不能为空")
|
||||||
}
|
}
|
||||||
conn, err := getDefaultDataSource()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !conn.getIsConnected() {
|
rc := getRedisConn(c.name)
|
||||||
if err := conn.redisReconnect(ctx); err != nil {
|
if !c.Ping(ctx) {
|
||||||
return fmt.Errorf("redis重连失败: %w", err)
|
if err := commonConnect(ctx, MessageRedis, c.name, func(ctx context.Context) error {
|
||||||
|
return c.Connect(ctx)
|
||||||
|
}, func(ctx context.Context) error {
|
||||||
|
return c.Close(ctx)
|
||||||
|
}); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", MessageRedis, c.name, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +100,7 @@ func (c *redis) Publish(ctx context.Context, msgConfig messagePublishConfig) err
|
|||||||
for key, val := range values {
|
for key, val := range values {
|
||||||
args = append(args, key, val)
|
args = append(args, key, val)
|
||||||
}
|
}
|
||||||
result, err := conn.getClient().Do(ctx, "XADD", args...)
|
result, err := rc.Do(ctx, "XADD", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.Log().Errorf(ctx, "❌ Redis 发布消息失败: key=%s, err=%v", cfg.QueueName, err)
|
g.Log().Errorf(ctx, "❌ Redis 发布消息失败: key=%s, err=%v", cfg.QueueName, err)
|
||||||
return err
|
return err
|
||||||
@@ -122,6 +109,12 @@ func (c *redis) Publish(ctx context.Context, msgConfig messagePublishConfig) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishDelay 发布延迟消息
|
||||||
|
func (c *redis) PublishDelay(ctx context.Context, _ messagePublishDelayConfig) error {
|
||||||
|
g.Log().Errorf(ctx, "❌ Redis 不支持延迟消息")
|
||||||
|
return fmt.Errorf("❌ Redis 不支持延迟消息")
|
||||||
|
}
|
||||||
|
|
||||||
// Subscribe 订阅消息
|
// Subscribe 订阅消息
|
||||||
func (c *redis) Subscribe(ctx context.Context, msgConfig messageSubscribeConfig) error {
|
func (c *redis) Subscribe(ctx context.Context, msgConfig messageSubscribeConfig) error {
|
||||||
cfg, ok := msgConfig.(*RedisSubscribeMsgConfig)
|
cfg, ok := msgConfig.(*RedisSubscribeMsgConfig)
|
||||||
@@ -142,162 +135,92 @@ func (c *redis) Subscribe(ctx context.Context, msgConfig messageSubscribeConfig)
|
|||||||
|
|
||||||
// createSubscribe 内部订阅消息
|
// createSubscribe 内部订阅消息
|
||||||
func (c *redis) createSubscribe(ctx context.Context, key, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
func (c *redis) createSubscribe(ctx context.Context, key, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
g.Log().Errorf(ctx, "❌ Redis 消费者 panic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
retryTicker := time.NewTicker(time.Second)
|
LOOP:
|
||||||
defer retryTicker.Stop()
|
err := c.consumeMessages(ctx, key, consumerName, prefetchCount, autoAck, handler)
|
||||||
|
if err != nil {
|
||||||
|
// 对于超时错误,返回nil继续循环,而不是返回错误
|
||||||
|
if strings.Contains(err.Error(), "i/o timeout") || strings.Contains(err.Error(), "timeout") ||
|
||||||
|
strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "context canceled") {
|
||||||
|
|
||||||
// 重试计数器
|
time.Sleep(time.Second)
|
||||||
var consecutiveErrors int
|
goto LOOP
|
||||||
const maxConsecutiveErrors = 3
|
} else {
|
||||||
|
g.Log().Errorf(ctx, "❌ 严重错误: %v", err)
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
g.Log().Infof(ctx, "🔕 Redis 消费者停止: topic=%s", key)
|
|
||||||
return
|
|
||||||
case <-retryTicker.C:
|
|
||||||
err := c.consumeMessages(ctx, key, consumerName, prefetchCount, autoAck, handler)
|
|
||||||
if err != nil {
|
|
||||||
// 对于超时错误,返回nil继续循环,而不是返回错误
|
|
||||||
if strings.Contains(err.Error(), "i/o timeout") || strings.Contains(err.Error(), "timeout") ||
|
|
||||||
strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "context canceled") {
|
|
||||||
|
|
||||||
consecutiveErrors++
|
|
||||||
if consecutiveErrors > maxConsecutiveErrors {
|
|
||||||
g.Log().Errorf(ctx, "Max retries exceeded, giving up")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
backoffTime := 5 * time.Second
|
|
||||||
g.Log().Warningf(ctx, "⚠️ 等待 %v 后重试...", backoffTime)
|
|
||||||
|
|
||||||
time.Sleep(backoffTime)
|
|
||||||
} else {
|
|
||||||
// 非超时错误(严重错误)
|
|
||||||
consecutiveErrors = 0 // 重置计数
|
|
||||||
g.Log().Errorf(ctx, "严重错误,立即重试: %v", err)
|
|
||||||
|
|
||||||
// 短暂等待后重试
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
// 继续循环
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 成功时重置错误计数器
|
|
||||||
consecutiveErrors = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
return nil
|
time.Sleep(time.Second)
|
||||||
|
goto LOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
// consumeMessages 消费消息
|
// consumeMessages 消费消息
|
||||||
func (c *redis) consumeMessages(ctx context.Context, key, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
func (c *redis) consumeMessages(ctx context.Context, key, consumerName string, prefetchCount int, autoAck bool, handler func(ctx context.Context, message map[string]interface{}) error) error {
|
||||||
conn, err := getDefaultDataSource()
|
if !c.Ping(ctx) {
|
||||||
if err != nil {
|
if err := commonConnect(ctx, MessageRedis, c.name, func(ctx context.Context) error {
|
||||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
return c.Connect(ctx)
|
||||||
|
}, func(ctx context.Context) error {
|
||||||
|
return c.Close(ctx)
|
||||||
|
}); err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ [%s][%s] 连接失败: %v", MessageRedis, c.name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !conn.getIsConnected() {
|
rc := getRedisConn(c.name)
|
||||||
if err := conn.redisReconnect(ctx); err != nil {
|
if rc == nil {
|
||||||
return fmt.Errorf("redis重连失败: %w", err)
|
g.Log().Errorf(ctx, "❌ Redis [%s] 连接不存在", c.name)
|
||||||
}
|
return fmt.Errorf("Redis 连接不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查消费者组是否存在
|
// 检查消费者组是否存在
|
||||||
if err := c.createStreamGroup(ctx, key); err != nil {
|
groupName := "default"
|
||||||
return fmt.Errorf("create stream group failed: %w", err)
|
_, err := rc.Do(ctx, "XGROUP", "CREATE", key, groupName, "0", "MKSTREAM")
|
||||||
|
if err != nil {
|
||||||
|
errStr := err.Error()
|
||||||
|
if strings.Contains(errStr, "BUSYGROUP") && strings.Contains(errStr, "already exists") {
|
||||||
|
glog.Infof(ctx, "✅ Redis [%s] 消费者组已存在: %s", c.name, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
g.Log().Errorf(ctx, "❌ 创建消费组失败: key=%s, err=%v", key, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
glog.Infof(ctx, "✅ Redis [%s] 消费者组创建成功: %s", c.name, key)
|
||||||
|
|
||||||
// 使用带重试的命令执行
|
// 使用带重试的命令执行
|
||||||
result, err := conn.getClient().Do(ctx, "XREADGROUP", "GROUP", "default", consumerName, "COUNT", prefetchCount, "BLOCK", 0, "STREAMS", key, ">")
|
result, err := rc.Do(ctx, "XREADGROUP", "GROUP", groupName, consumerName, "COUNT", prefetchCount, "BLOCK", 0, "STREAMS", key, ">")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "i/o timeout") || strings.Contains(err.Error(), "timeout") ||
|
|
||||||
strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "context canceled") {
|
|
||||||
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
messages, err := c.parseStreamResult(result)
|
messages, err := c.parseStreamResult(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ 解析消息失败: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
// 处理消息
|
// 处理消息
|
||||||
if err := handler(ctx, msg.Values); err != nil {
|
if err := handler(ctx, msg.Values); err != nil {
|
||||||
g.Log().Errorf(ctx, "❌ 消息处理失败: messageID=%s, err=%v", msg.ID, err)
|
g.Log().Errorf(ctx, "❌ 消息处理失败: messageID=%s, err=%v", msg.ID, err)
|
||||||
continue
|
// 如果不是自动ACK,则跳过当前消息
|
||||||
}
|
if !autoAck {
|
||||||
|
continue
|
||||||
// ACK 消息
|
|
||||||
if autoAck {
|
|
||||||
if err := c.ackMessage(ctx, key, "default", msg.ID); err != nil {
|
|
||||||
g.Log().Errorf(ctx, "❌ ACK 消息失败: messageID=%s, err=%v", msg.ID, err)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
g.Log().Infof(ctx, "✅ 消息处理成功: messageID=%s", msg.ID)
|
||||||
|
}
|
||||||
|
// ACK 消息
|
||||||
|
args := make([]interface{}, 0, len(msg.ID)+2)
|
||||||
|
args = append(args, key, groupName, msg.ID)
|
||||||
|
_, err = rc.Do(ctx, "XACK", args...)
|
||||||
|
if err != nil {
|
||||||
|
g.Log().Errorf(ctx, "❌ ACK 消息失败: messageID=%s, err=%v", msg.ID, err)
|
||||||
|
} else {
|
||||||
|
g.Log().Infof(ctx, "✅ ACK 消息成功: messageID=%s", msg.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createStreamGroup 内部单个创建消费组
|
|
||||||
func (c *redis) createStreamGroup(ctx context.Context, key string) error {
|
|
||||||
conn, err := getDefaultDataSource()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !conn.getIsConnected() {
|
|
||||||
if err := conn.redisReconnect(ctx); err != nil {
|
|
||||||
return fmt.Errorf("redis重连失败: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
groupName := "default"
|
|
||||||
_, err = conn.getClient().Do(ctx, "XGROUP", "CREATE", key, groupName, "0", "MKSTREAM")
|
|
||||||
if err != nil {
|
|
||||||
errStr := err.Error()
|
|
||||||
if strings.Contains(errStr, "BUSYGROUP") && strings.Contains(errStr, "already exists") {
|
|
||||||
glog.Infof(ctx, "✅ Redis 消费者组已存在: %s", groupName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("初始化消费者组失败: %w", err)
|
|
||||||
}
|
|
||||||
glog.Infof(ctx, "✅ Redis 消费者组创建成功: %s", groupName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ackMessage ACK 消息
|
|
||||||
func (c *redis) ackMessage(ctx context.Context, streamKey, groupName string, messageIDs ...string) error {
|
|
||||||
conn, err := getDefaultDataSource()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取默认连接失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !conn.getIsConnected() {
|
|
||||||
if err := conn.redisReconnect(ctx); err != nil {
|
|
||||||
return fmt.Errorf("redis重连失败: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args := make([]interface{}, 0, len(messageIDs)+2)
|
|
||||||
args = append(args, streamKey, groupName)
|
|
||||||
for _, id := range messageIDs {
|
|
||||||
args = append(args, id)
|
|
||||||
}
|
|
||||||
_, err = conn.getClient().Do(ctx, "XACK", args...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseStreamResult 解析 Stream 结果
|
// parseStreamResult 解析 Stream 结果
|
||||||
func (c *redis) parseStreamResult(result interface{}) ([]redisStreamMessage, error) {
|
func (c *redis) parseStreamResult(result interface{}) ([]redisStreamMessage, error) {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
|||||||
125
message/store.go
Normal file
125
message/store.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Copyright 2019-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package message
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type RetentionPolicy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LimitsPolicy (default) means that messages are retained until any given limit is reached.
|
||||||
|
// This could be one of MaxMsgs, MaxBytes, or MaxAge.
|
||||||
|
LimitsPolicy RetentionPolicy = iota
|
||||||
|
// InterestPolicy specifies that when all known consumers have acknowledged a message it can be removed.
|
||||||
|
InterestPolicy
|
||||||
|
// WorkQueuePolicy specifies that when the first worker or subscriber acknowledges the message it can be removed.
|
||||||
|
WorkQueuePolicy
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalJSON 将 RetentionPolicy 序列化为字符串
|
||||||
|
func (rp RetentionPolicy) MarshalJSON() ([]byte, error) {
|
||||||
|
switch rp {
|
||||||
|
case LimitsPolicy:
|
||||||
|
return []byte(`"limits"`), nil
|
||||||
|
case InterestPolicy:
|
||||||
|
return []byte(`"interest"`), nil
|
||||||
|
case WorkQueuePolicy:
|
||||||
|
return []byte(`"workqueue"`), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("can not marshal %v", rp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 将字符串反序列化为 RetentionPolicy
|
||||||
|
func (rp *RetentionPolicy) UnmarshalJSON(data []byte) error {
|
||||||
|
switch string(data) {
|
||||||
|
case `"limits"`:
|
||||||
|
*rp = LimitsPolicy
|
||||||
|
case `"interest"`:
|
||||||
|
*rp = InterestPolicy
|
||||||
|
case `"workqueue"`:
|
||||||
|
*rp = WorkQueuePolicy
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown retention policy: %s", string(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscardPolicy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DiscardOld will remove older messages to return to the limits.
|
||||||
|
DiscardOld = iota
|
||||||
|
// DiscardNew will error on a StoreMsg call
|
||||||
|
DiscardNew
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalJSON 将 DiscardPolicy 序列化为字符串
|
||||||
|
func (dp DiscardPolicy) MarshalJSON() ([]byte, error) {
|
||||||
|
switch dp {
|
||||||
|
case DiscardOld:
|
||||||
|
return []byte(`"old"`), nil
|
||||||
|
case DiscardNew:
|
||||||
|
return []byte(`"new"`), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("can not marshal %v", dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 将字符串反序列化为 DiscardPolicy
|
||||||
|
func (dp *DiscardPolicy) UnmarshalJSON(data []byte) error {
|
||||||
|
switch string(data) {
|
||||||
|
case `"old"`:
|
||||||
|
*dp = DiscardOld
|
||||||
|
case `"new"`:
|
||||||
|
*dp = DiscardNew
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown discard policy: %s", string(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FileStorage specifies on disk, designated by the JetStream config StoreDir.
|
||||||
|
FileStorage = StorageType(22)
|
||||||
|
// MemoryStorage specifies in memory only.
|
||||||
|
MemoryStorage = StorageType(33)
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalJSON 将 StorageType 序列化为字符串
|
||||||
|
func (st StorageType) MarshalJSON() ([]byte, error) {
|
||||||
|
switch st {
|
||||||
|
case MemoryStorage:
|
||||||
|
return []byte(`"memory"`), nil
|
||||||
|
case FileStorage:
|
||||||
|
return []byte(`"file"`), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("can not marshal %v", st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 将字符串反序列化为 StorageType
|
||||||
|
func (st *StorageType) UnmarshalJSON(data []byte) error {
|
||||||
|
switch string(data) {
|
||||||
|
case `"memory"`:
|
||||||
|
*st = MemoryStorage
|
||||||
|
case `"file"`:
|
||||||
|
*st = FileStorage
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown storage type: %s", string(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
212
message/stream.go
Normal file
212
message/stream.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// Copyright 2019-2026 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StreamConfig will determine the name, subjects and retention policy
|
||||||
|
// for a given stream. If subjects is empty the name will be used.
|
||||||
|
type StreamConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Subjects []string `json:"subjects,omitempty"`
|
||||||
|
Retention RetentionPolicy `json:"retention"`
|
||||||
|
MaxConsumers int `json:"max_consumers"`
|
||||||
|
MaxMsgs int64 `json:"max_msgs"`
|
||||||
|
MaxBytes int64 `json:"max_bytes"`
|
||||||
|
MaxAge time.Duration `json:"max_age"`
|
||||||
|
MaxMsgsPer int64 `json:"max_msgs_per_subject"`
|
||||||
|
MaxMsgSize int32 `json:"max_msg_size,omitempty"`
|
||||||
|
Discard DiscardPolicy `json:"discard"`
|
||||||
|
Storage StorageType `json:"storage"`
|
||||||
|
Replicas int `json:"num_replicas"`
|
||||||
|
NoAck bool `json:"no_ack,omitempty"`
|
||||||
|
Duplicates time.Duration `json:"duplicate_window,omitempty"`
|
||||||
|
Placement *Placement `json:"placement,omitempty"`
|
||||||
|
Mirror *StreamSource `json:"mirror,omitempty"`
|
||||||
|
Sources []*StreamSource `json:"sources,omitempty"`
|
||||||
|
Compression StoreCompression `json:"compression"`
|
||||||
|
FirstSeq uint64 `json:"first_seq,omitempty"`
|
||||||
|
|
||||||
|
// Allow applying a subject transform to incoming messages before doing anything else
|
||||||
|
SubjectTransform *SubjectTransformConfig `json:"subject_transform,omitempty"`
|
||||||
|
|
||||||
|
// Allow republish of the message after being sequenced and stored.
|
||||||
|
RePublish *RePublish `json:"republish,omitempty"`
|
||||||
|
|
||||||
|
// Allow higher performance, direct access to get individual messages. E.g. KeyValue
|
||||||
|
AllowDirect bool `json:"allow_direct"`
|
||||||
|
// Allow higher performance and unified direct access for mirrors as well.
|
||||||
|
MirrorDirect bool `json:"mirror_direct"`
|
||||||
|
|
||||||
|
// Allow KV like semantics to also discard new on a per subject basis
|
||||||
|
DiscardNewPer bool `json:"discard_new_per_subject,omitempty"`
|
||||||
|
|
||||||
|
// Optional qualifiers. These can not be modified after set to true.
|
||||||
|
|
||||||
|
// Sealed will seal a stream so no messages can get out or in.
|
||||||
|
Sealed bool `json:"sealed"`
|
||||||
|
// DenyDelete will restrict the ability to delete messages.
|
||||||
|
DenyDelete bool `json:"deny_delete"`
|
||||||
|
// DenyPurge will restrict the ability to purge messages.
|
||||||
|
DenyPurge bool `json:"deny_purge"`
|
||||||
|
// AllowRollup allows messages to be placed into the system and purge
|
||||||
|
// all older messages using a special msg header.
|
||||||
|
AllowRollup bool `json:"allow_rollup_hdrs"`
|
||||||
|
|
||||||
|
// The following defaults will apply to consumers when created against
|
||||||
|
// this stream, unless overridden manually.
|
||||||
|
// TODO(nat): Can/should we name these better?
|
||||||
|
ConsumerLimits StreamConsumerLimits `json:"consumer_limits"`
|
||||||
|
|
||||||
|
// AllowMsgTTL allows header initiated per-message TTLs. If disabled,
|
||||||
|
// then the `NATS-TTL` header will be ignored.
|
||||||
|
AllowMsgTTL bool `json:"allow_msg_ttl"`
|
||||||
|
|
||||||
|
// SubjectDeleteMarkerTTL sets the TTL of delete marker messages left behind by
|
||||||
|
// subject delete markers.
|
||||||
|
SubjectDeleteMarkerTTL time.Duration `json:"subject_delete_marker_ttl,omitempty"`
|
||||||
|
|
||||||
|
// AllowMsgCounter allows a stream to use (only) counter CRDTs.
|
||||||
|
AllowMsgCounter bool `json:"allow_msg_counter,omitempty"`
|
||||||
|
|
||||||
|
// AllowAtomicPublish allows atomic batch publishing into the stream.
|
||||||
|
AllowAtomicPublish bool `json:"allow_atomic,omitempty"`
|
||||||
|
|
||||||
|
// AllowMsgSchedules allows the scheduling of messages.
|
||||||
|
AllowMsgSchedules bool `json:"allow_msg_schedules,omitempty"`
|
||||||
|
|
||||||
|
// PersistMode allows to opt-in to different persistence mode settings.
|
||||||
|
PersistMode PersistModeType `json:"persist_mode,omitempty"`
|
||||||
|
|
||||||
|
// Metadata is additional metadata for the Stream.
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to guide placement of streams and meta controllers in clustered JetStream.
|
||||||
|
type Placement struct {
|
||||||
|
Cluster string `json:"cluster,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
Preferred string `json:"preferred,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamSource dictates how streams can source from other streams.
|
||||||
|
type StreamSource struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
OptStartSeq uint64 `json:"opt_start_seq,omitempty"`
|
||||||
|
OptStartTime *time.Time `json:"opt_start_time,omitempty"`
|
||||||
|
FilterSubject string `json:"filter_subject,omitempty"`
|
||||||
|
SubjectTransforms []SubjectTransformConfig `json:"subject_transforms,omitempty"`
|
||||||
|
External *ExternalStream `json:"external,omitempty"`
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
iname string // For indexing when stream names are the same for multiple sources.
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubjectTransformConfig is for applying a subject transform (to matching messages) before doing anything else when a new message is received
|
||||||
|
type SubjectTransformConfig struct {
|
||||||
|
Source string `json:"src"`
|
||||||
|
Destination string `json:"dest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalStream allows you to qualify access to a stream source in another account or domain.
|
||||||
|
type ExternalStream struct {
|
||||||
|
ApiPrefix string `json:"api"`
|
||||||
|
DeliverPrefix string `json:"deliver"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RePublish is for republishing messages once committed to a stream.
|
||||||
|
type RePublish struct {
|
||||||
|
Source string `json:"src,omitempty"`
|
||||||
|
Destination string `json:"dest"`
|
||||||
|
HeadersOnly bool `json:"headers_only,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamConsumerLimits struct {
|
||||||
|
InactiveThreshold time.Duration `json:"inactive_threshold,omitempty"`
|
||||||
|
MaxAckPending int `json:"max_ack_pending,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersistModeType determines what persistence mode the stream uses.
|
||||||
|
type PersistModeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultPersistMode specifies the default persist mode. Writes to the stream will immediately be flushed.
|
||||||
|
// The publish acknowledgement will be sent after the persisting completes.
|
||||||
|
DefaultPersistMode = PersistModeType(iota)
|
||||||
|
// AsyncPersistMode specifies writes to the stream will be flushed asynchronously.
|
||||||
|
// The publish acknowledgement may be sent before the persisting completes.
|
||||||
|
// This means writes could be lost if they weren't flushed prior to a hard kill of the server.
|
||||||
|
AsyncPersistMode
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalJSON 将 PersistModeType 序列化为字符串
|
||||||
|
func (pm PersistModeType) MarshalJSON() ([]byte, error) {
|
||||||
|
switch pm {
|
||||||
|
case DefaultPersistMode:
|
||||||
|
return []byte(`"default"`), nil
|
||||||
|
case AsyncPersistMode:
|
||||||
|
return []byte(`"async"`), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("can not marshal %v", pm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 将字符串反序列化为 PersistModeType
|
||||||
|
func (pm *PersistModeType) UnmarshalJSON(data []byte) error {
|
||||||
|
switch string(data) {
|
||||||
|
case `"default"`:
|
||||||
|
*pm = DefaultPersistMode
|
||||||
|
case `"async"`:
|
||||||
|
*pm = AsyncPersistMode
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown persist mode: %s", string(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreCompression uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
NoCompression StoreCompression = iota
|
||||||
|
S2Compression
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalJSON 将 StoreCompression 序列化为字符串
|
||||||
|
func (sc StoreCompression) MarshalJSON() ([]byte, error) {
|
||||||
|
switch sc {
|
||||||
|
case NoCompression:
|
||||||
|
return []byte(`"none"`), nil
|
||||||
|
case S2Compression:
|
||||||
|
return []byte(`"s2"`), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("can not marshal %v", sc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 将字符串反序列化为 StoreCompression
|
||||||
|
func (sc *StoreCompression) UnmarshalJSON(data []byte) error {
|
||||||
|
switch string(data) {
|
||||||
|
case `"none"`:
|
||||||
|
*sc = NoCompression
|
||||||
|
case `"s2"`:
|
||||||
|
*sc = S2Compression
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown store compression: %s", string(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user