9 Commits
19 changed files with 839 additions and 2285 deletions
+18
View File
@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2025 blueocean-go
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+146 -375
View File
@@ -1,447 +1,218 @@
# Reconnect - Go 连接重连库 # reconnect
[![Go Version](https://img.shields.io/badge/Go-1.24+-blue.svg)](https://golang.org) 一个通用的Go语言重连库,支持Redis、PostgreSQL和etcd的自动重连功能。
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
一个用于 Go 语言的智能连接重连库,支持数据库、Redis、etcd、gRPC 等多种服务的自动重连和健康检查。
## 特性 ## 特性
- 🔄 **智能重连**: 支持多种重连策略(指数退避、固定间隔、线性退避) - 🔄 自动重连机制
- 💗 **健康检查**: 定期检查连接状态,自动发现问题并重连 - ⚙️ 可配置的重连策略
- 🔌 **多服务支持**: 内置支持数据库(GORM)、Redis、etcd、gRPC - 🔍 连接健康检查
- ⚙️ **高度可配置**: 灵活的配置选项,满足不同场景需求 - 📊 连接状态监控
- 📊 **状态监控**: 实时连接状态监控和回调通知 - 🎯 支持多种服务(Redis、PostgreSQL、etcd
- 🛡️ **线程安全**: 完全的并发安全设计 - 🛡️ 线程安全
## 支持的服务
- **数据库**: 通过 GORM 支持 MySQL、PostgreSQL、SQLite 等
- **Redis**: 基于 go-redis/v8 客户端
- **etcd**: 基于 etcd client/v3
- **gRPC**: 基于 google.golang.org/grpc
## 安装 ## 安装
```bash ```bash
go get git.whblueocean.cn/blueocean-go/reconnect go get github.com/blueocean-go/reconnect
``` ```
## 快速开始 ## 快速开始
### 数据库重连 ### Redis重连
```go ```go
package main package main
import ( import (
"fmt" "context"
"log"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"git.whblueocean.cn/blueocean-go/reconnect"
)
func main() {
// 配置数据库连接
config := reconnect.DatabaseConfig{
Dialector: postgres.Open("host=localhost user=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"),
GormConfig: &gorm.Config{},
MaxIdleConns: 10,
MaxOpenConns: 100,
ConnMaxLifetime: time.Hour,
ReconnectConfig: reconnect.Config{
Strategy: reconnect.StrategyExponentialBackoff,
MaxRetries: -1, // 无限重试
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Multiplier: 2.0,
HealthCheckInterval: 10 * time.Second,
HealthCheckTimeout: 5 * time.Second,
OnStateChange: func(oldState, newState reconnect.ConnectionState) {
log.Printf("数据库状态变化: %s -> %s", oldState, newState)
},
OnError: func(err error) {
log.Printf("数据库错误: %v", err)
},
},
}
// 创建客户端
client, err := reconnect.NewDatabaseClient(config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 使用数据库
db := client.GetDB()
// ... 执行数据库操作
}
```
### Redis 重连
```go
package main
import (
"log"
"time" "time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"git.whblueocean.cn/blueocean-go/reconnect" "github.com/blueocean-go/reconnect"
) )
func main() { func main() {
// 配置 Redis 连接 options := &redis.Options{
config := reconnect.RedisClientConfig{ Addr: "localhost:6379",
Options: &redis.Options{ Password: "",
Addr: "localhost:6379", DB: 0,
Password: "", // 无密码
DB: 0, // 默认数据库
},
ReconnectConfig: reconnect.Config{
Strategy: reconnect.StrategyFixedInterval,
MaxRetries: 10,
InitialDelay: 2 * time.Second,
HealthCheckInterval: 15 * time.Second,
OnReconnect: func(attempt int) {
log.Printf("Redis 重连成功,尝试次数: %d", attempt)
},
},
} }
// 创建客户端 config := reconnect.DefaultConfig()
client, err := reconnect.NewRedisClient(config) config.RetryInterval = 3 * time.Second
if err != nil { config.OnReconnect = func() {
fmt.Println("Redis重连成功!")
}
manager := reconnect.RedisManager(options, config)
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer client.Close() defer manager.Stop()
// 使用 Redis // 使用Redis客户端
rdb := client.GetClient() redisClient := reconnect.NewRedisClient(options)
// ... 执行 Redis 操作 redisClient.Connect(ctx)
client := redisClient.GetClient()
// ... 使用client进行操作
} }
``` ```
### etcd 重连 ### PostgreSQL重连
```go ```go
package main package main
import ( import (
"log" "context"
"time" "time"
"git.whblueocean.cn/blueocean-go/reconnect" "github.com/blueocean-go/reconnect"
) )
func main() { func main() {
// 配置 etcd 连接 dsn := "host=localhost user=postgres password=postgres dbname=testdb sslmode=disable"
config := reconnect.EtcdClientConfig{
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.OnReconnect = func() {
fmt.Println("PostgreSQL重连成功!")
}
manager := reconnect.PostgresManager(
dsn,
25, // maxConnections
5, // maxIdleConnections
30*time.Minute, // maxConnectionAge
config,
)
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatal(err)
}
defer manager.Stop()
// 使用数据库连接
pgClient := reconnect.NewPostgresClient(dsn, 25, 5, 30*time.Minute)
pgClient.Connect(ctx)
db := pgClient.GetDB()
// ... 使用db进行操作
}
```
### etcd重连
```go
package main
import (
"context"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"github.com/blueocean-go/reconnect"
)
func main() {
etcdConfig := clientv3.Config{
Endpoints: []string{"localhost:2379"}, Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second, DialTimeout: 5 * time.Second,
Username: "",
Password: "",
ReconnectConfig: reconnect.Config{
Strategy: reconnect.StrategyLinearBackoff,
MaxRetries: -1,
InitialDelay: 1 * time.Second,
MaxDelay: 20 * time.Second,
LinearIncrement: 1 * time.Second,
HealthCheckInterval: 30 * time.Second,
},
} }
// 创建客户端 config := reconnect.DefaultConfig()
client, err := reconnect.NewEtcdClient(config) config.RetryInterval = 3 * time.Second
if err != nil { config.OnReconnect = func() {
fmt.Println("etcd重连成功!")
}
manager := reconnect.EtcdManager(etcdConfig, config)
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer client.Close() defer manager.Stop()
// 使用 etcd // 使用etcd客户端
etcdClient := client.GetClient() etcdClient := reconnect.NewEtcdClient(etcdConfig)
// ... 执行 etcd 操作 etcdClient.Connect(ctx)
} client := etcdClient.GetClient()
``` // ... 使用client进行操作
### gRPC 重连
```go
package main
import (
"log"
"time"
"google.golang.org/grpc"
"git.whblueocean.cn/blueocean-go/reconnect"
)
func main() {
// 配置 gRPC 连接
config := reconnect.GRPCClientConfig{
Target: "localhost:50051",
DialOptions: []grpc.DialOption{
grpc.WithInsecure(),
// 其他 gRPC 选项...
},
ReconnectConfig: reconnect.Config{
Strategy: reconnect.StrategyExponentialBackoff,
MaxRetries: 5,
InitialDelay: 1 * time.Second,
MaxDelay: 60 * time.Second,
Multiplier: 1.5,
HealthCheckInterval: 20 * time.Second,
},
}
// 创建客户端
client, err := reconnect.NewGRPCClient(config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 使用 gRPC 连接
conn := client.GetConn()
// ... 创建 gRPC 客户端并调用服务
}
```
### RabbitMQ 重连
```go
package main
import (
"log"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"git.whblueocean.cn/blueocean-go/reconnect"
)
func main() {
// 配置 RabbitMQ 连接
config := reconnect.RabbitMQClientConfig{
URL: "amqp://guest:guest@localhost:5672/",
ReconnectConfig: reconnect.Config{
Strategy: reconnect.StrategyExponentialBackoff,
MaxRetries: -1, // 无限重试
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Multiplier: 2.0,
HealthCheckInterval: 15 * time.Second,
OnStateChange: func(oldState, newState reconnect.ConnectionState) {
log.Printf("RabbitMQ 状态变化: %s -> %s", oldState, newState)
},
OnError: func(err error) {
log.Printf("RabbitMQ 错误: %v", err)
},
},
}
// 创建客户端
client, err := reconnect.NewRabbitMQClient(config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 声明队列
queue, err := client.QueueDeclare(
"hello", // 队列名
true, // 持久化
false, // 自动删除
false, // 独占
false, // 不等待
nil, // 参数
)
if err != nil {
log.Fatal(err)
}
// 发布消息
err = client.Publish(
"", // 交换机
queue.Name, // 路由键
false, // 强制
false, // 立即
amqp.Publishing{
ContentType: "text/plain",
Body: []byte("Hello World!"),
},
)
if err != nil {
log.Fatal(err)
}
// 消费消息
msgs, err := client.Consume(
queue.Name, // 队列
"", // 消费者
true, // 自动确认
false, // 独占
false, // 不等待
false, // 参数
nil,
)
if err != nil {
log.Fatal(err)
}
// 处理消息
for msg := range msgs {
log.Printf("收到消息: %s", msg.Body)
}
}
```
## 重连策略
### 指数退避策略 (Exponential Backoff)
默认策略,每次重试间隔按指数增长,避免对服务器造成过大压力。
```go
config := reconnect.Config{
Strategy: reconnect.StrategyExponentialBackoff,
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Multiplier: 2.0, // 每次延迟翻倍
}
```
### 固定间隔策略 (Fixed Interval)
每次重试使用固定的时间间隔。
```go
config := reconnect.Config{
Strategy: reconnect.StrategyFixedInterval,
InitialDelay: 5 * time.Second, // 固定 5 秒间隔
}
```
### 线性退避策略 (Linear Backoff)
每次重试间隔线性增长。
```go
config := reconnect.Config{
Strategy: reconnect.StrategyLinearBackoff,
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
LinearIncrement: 2 * time.Second, // 每次增加 2 秒
} }
``` ```
## 配置选项 ## 配置选项
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `Strategy` | `StrategyType` | `ExponentialBackoff` | 重连策略类型 |
| `MaxRetries` | `int` | `-1` | 最大重试次数,-1 表示无限重试 |
| `InitialDelay` | `time.Duration` | `1s` | 初始延迟时间 |
| `MaxDelay` | `time.Duration` | `30s` | 最大延迟时间 |
| `Multiplier` | `float64` | `2.0` | 指数退避乘数 |
| `LinearIncrement` | `time.Duration` | `1s` | 线性退避增量 |
| `HealthCheckInterval` | `time.Duration` | `10s` | 健康检查间隔 |
| `HealthCheckTimeout` | `time.Duration` | `5s` | 健康检查超时时间 |
| `OnStateChange` | `func(ConnectionState, ConnectionState)` | `nil` | 状态变化回调 |
| `OnReconnect` | `func(int)` | `nil` | 重连成功回调 |
| `OnError` | `func(error)` | `nil` | 错误回调 |
## 连接状态
- `StateDisconnected`: 断开连接
- `StateConnecting`: 正在连接
- `StateConnected`: 已连接
- `StateReconnecting`: 正在重连
## 高级用法
### 自定义连接器
如果需要支持其他类型的连接,可以实现 `Connector``HealthChecker` 接口:
```go ```go
type CustomConnector struct { type Config struct {
// 自定义连接逻辑 // MaxRetries 最大重试次数,0表示无限重试
} MaxRetries int
func (c *CustomConnector) Connect(ctx context.Context) error { // RetryInterval 重试间隔
// 实现连接逻辑 RetryInterval time.Duration
return nil
}
func (c *CustomConnector) Close() error { // Timeout 连接超时时间
// 实现关闭逻辑 Timeout time.Duration
return nil
}
type CustomHealthChecker struct { // OnReconnect 重连成功后的回调函数
// 自定义健康检查逻辑 OnReconnect func()
}
func (c *CustomHealthChecker) HealthCheck(ctx context.Context) error { // OnDisconnect 断开连接时的回调函数
// 实现健康检查逻辑 OnDisconnect func(error)
return nil
} }
// 使用自定义连接器
connector := &CustomConnector{}
checker := &CustomHealthChecker{}
manager := reconnect.NewConnectionManager(connector, checker, config)
``` ```
### 手动触发重连 ## API文档
```go ### Manager
// 手动触发重连
client.TriggerReconnect()
// 获取当前状态 重连管理器,负责监控连接状态并自动重连。
state := client.State()
fmt.Printf("当前状态: %s\n", state)
```
## 最佳实践 - `Start(ctx context.Context) error` - 启动连接并开始监控
- `Stop() error` - 停止重连管理器
- `IsConnected() bool` - 返回当前连接状态
- `WaitForError() error` - 等待错误(用于阻塞等待)
1. **合理设置重试次数**: 对于关键服务建议设置为无限重试,非关键服务设置合理的重试上限 ### RedisClient
2. **配置适当的健康检查间隔**: 太频繁会影响性能,太稀疏会延迟发现问题
3. **使用回调函数监控状态**: 通过 `OnStateChange``OnReconnect``OnError` 回调监控连接状态
4. **设置合适的超时时间**: 避免长时间阻塞
5. **优雅关闭**: 在应用关闭时调用 `Close()` 方法释放资源
## 依赖 Redis客户端包装器。
- [go-redis/v8](https://github.com/go-redis/redis) - Redis 客户端 - `Connect(ctx context.Context) error` - 建立连接
- [etcd client/v3](https://etcd.io/) - etcd 客户端 - `Close() error` - 关闭连接
- [google.golang.org/grpc](https://grpc.io/) - gRPC 框架 - `Ping(ctx context.Context) error` - 健康检查
- [gorm.io/gorm](https://gorm.io/) - ORM 框架 - `IsConnected() bool` - 检查连接状态
- `GetClient() *redis.Client` - 获取底层Redis客户端
### PostgresClient
PostgreSQL客户端包装器。
- `Connect(ctx context.Context) error` - 建立连接
- `Close() error` - 关闭连接
- `Ping(ctx context.Context) error` - 健康检查
- `IsConnected() bool` - 检查连接状态
- `GetDB() *sql.DB` - 获取底层数据库连接
### EtcdClient
etcd客户端包装器。
- `Connect(ctx context.Context) error` - 建立连接
- `Close() error` - 关闭连接
- `Ping(ctx context.Context) error` - 健康检查
- `IsConnected() bool` - 检查连接状态
- `GetClient() *clientv3.Client` - 获取底层etcd客户端
## 示例
更多示例代码请查看 `examples/` 目录。
## 许可证 ## 许可证
MIT License MIT License
## 贡献 Copyright (c) 2025 blueocean-go
欢迎提交 Issue 和 Pull Request
## 更新日志
### v1.1.0
- 新增 RabbitMQ 重连支持
- 添加 RabbitMQ 客户端封装和健康检查
- 完善文档和使用示例
### v1.0.0
- 初始版本发布
- 支持数据库、Redis、etcd、gRPC 重连
- 实现三种重连策略
-117
View File
@@ -1,117 +0,0 @@
package reconnect
import "time"
// StrategyType 定义重连策略类型
type StrategyType string
const (
// StrategyExponentialBackoff 指数退避策略
StrategyExponentialBackoff StrategyType = "exponential"
// StrategyFixedInterval 固定间隔策略
StrategyFixedInterval StrategyType = "fixed"
// StrategyLinearBackoff 线性退避策略
StrategyLinearBackoff StrategyType = "linear"
)
// ConnectionState 连接状态
type ConnectionState int
const (
// StateDisconnected 断开连接
StateDisconnected ConnectionState = iota
// StateConnecting 正在连接
StateConnecting
// StateConnected 已连接
StateConnected
// StateReconnecting 正在重连
StateReconnecting
)
func (s ConnectionState) String() string {
switch s {
case StateDisconnected:
return "disconnected"
case StateConnecting:
return "connecting"
case StateConnected:
return "connected"
case StateReconnecting:
return "reconnecting"
default:
return "unknown"
}
}
// Config 重连配置
type Config struct {
// Strategy 重连策略类型
Strategy StrategyType
// MaxRetries 最大重试次数,-1 表示无限重试
MaxRetries int
// InitialDelay 初始延迟时间
InitialDelay time.Duration
// MaxDelay 最大延迟时间
MaxDelay time.Duration
// Multiplier 指数退避乘数(仅用于指数退避策略)
Multiplier float64
// LinearIncrement 线性增量(仅用于线性退避策略)
LinearIncrement time.Duration
// HealthCheckInterval 健康检查间隔
HealthCheckInterval time.Duration
// HealthCheckTimeout 健康检查超时时间
HealthCheckTimeout time.Duration
// OnStateChange 状态变化回调函数
OnStateChange func(oldState, newState ConnectionState)
// OnReconnect 重连回调函数(重连成功时调用)
OnReconnect func(attempt int)
// OnError 错误回调函数
OnError func(err error)
}
// DefaultConfig 返回默认配置
func DefaultConfig() Config {
return Config{
Strategy: StrategyExponentialBackoff,
MaxRetries: -1, // 无限重试
InitialDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
Multiplier: 2.0,
LinearIncrement: 1 * time.Second,
HealthCheckInterval: 10 * time.Second,
HealthCheckTimeout: 5 * time.Second,
}
}
// Validate 验证配置
func (c *Config) Validate() {
if c.InitialDelay <= 0 {
c.InitialDelay = 1 * time.Second
}
if c.MaxDelay <= 0 {
c.MaxDelay = 30 * time.Second
}
if c.Multiplier <= 0 {
c.Multiplier = 2.0
}
if c.LinearIncrement <= 0 {
c.LinearIncrement = 1 * time.Second
}
if c.HealthCheckInterval <= 0 {
c.HealthCheckInterval = 10 * time.Second
}
if c.HealthCheckTimeout <= 0 {
c.HealthCheckTimeout = 5 * time.Second
}
}
-170
View File
@@ -1,170 +0,0 @@
package reconnect
import (
"context"
"database/sql"
"sync"
"time"
"gorm.io/gorm"
)
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
// Dialector gorm dialector(如 postgres.Open(dsn), mysql.Open(dsn)
Dialector gorm.Dialector
// GormConfig gorm配置
GormConfig *gorm.Config
// MaxIdleConns 最大空闲连接数
MaxIdleConns int
// MaxOpenConns 最大打开连接数
MaxOpenConns int
// ConnMaxLifetime 连接最大生命周期
ConnMaxLifetime time.Duration
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// DatabaseClient 带重连功能的数据库客户端
type DatabaseClient struct {
config DatabaseConfig
db *gorm.DB
sqlDB *sql.DB
manager *ConnectionManager
mu sync.RWMutex
}
// databaseConnector 数据库连接器
type databaseConnector struct {
client *DatabaseClient
}
func (d *databaseConnector) Connect(ctx context.Context) error {
d.client.mu.Lock()
defer d.client.mu.Unlock()
gormConfig := d.client.config.GormConfig
if gormConfig == nil {
gormConfig = &gorm.Config{}
}
db, err := gorm.Open(d.client.config.Dialector, gormConfig)
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
// 配置连接池
if d.client.config.MaxIdleConns > 0 {
sqlDB.SetMaxIdleConns(d.client.config.MaxIdleConns)
}
if d.client.config.MaxOpenConns > 0 {
sqlDB.SetMaxOpenConns(d.client.config.MaxOpenConns)
}
if d.client.config.ConnMaxLifetime > 0 {
sqlDB.SetConnMaxLifetime(d.client.config.ConnMaxLifetime)
}
// 验证连接
if err := sqlDB.PingContext(ctx); err != nil {
sqlDB.Close()
return err
}
d.client.db = db
d.client.sqlDB = sqlDB
return nil
}
func (d *databaseConnector) Close() error {
d.client.mu.Lock()
defer d.client.mu.Unlock()
if d.client.sqlDB != nil {
err := d.client.sqlDB.Close()
d.client.db = nil
d.client.sqlDB = nil
return err
}
return nil
}
// databaseHealthChecker 数据库健康检查器
type databaseHealthChecker struct {
client *DatabaseClient
}
func (d *databaseHealthChecker) HealthCheck(ctx context.Context) error {
d.client.mu.RLock()
sqlDB := d.client.sqlDB
d.client.mu.RUnlock()
if sqlDB == nil {
return context.Canceled
}
return sqlDB.PingContext(ctx)
}
// NewDatabaseClient 创建带重连功能的数据库客户端
func NewDatabaseClient(cfg DatabaseConfig) (*DatabaseClient, error) {
client := &DatabaseClient{
config: cfg,
}
connector := &databaseConnector{client: client}
checker := &databaseHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
}
// GetDB 获取gorm.DB实例
func (c *DatabaseClient) GetDB() *gorm.DB {
c.mu.RLock()
defer c.mu.RUnlock()
return c.db
}
// GetSqlDB 获取sql.DB实例
func (c *DatabaseClient) GetSqlDB() *sql.DB {
c.mu.RLock()
defer c.mu.RUnlock()
return c.sqlDB
}
// State 获取连接状态
func (c *DatabaseClient) State() ConnectionState {
return c.manager.State()
}
// Close 关闭客户端
func (c *DatabaseClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *DatabaseClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
// Ping 执行Ping命令
func (c *DatabaseClient) Ping(ctx context.Context) error {
sqlDB := c.GetSqlDB()
if sqlDB == nil {
return context.Canceled
}
return sqlDB.PingContext(ctx)
}
+60 -96
View File
@@ -2,143 +2,107 @@ package reconnect
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"time" "time"
clientv3 "go.etcd.io/etcd/client/v3" clientv3 "go.etcd.io/etcd/client/v3"
) )
// EtcdClientConfig etcd客户端配置 // EtcdClient etcd客户端包装器
type EtcdClientConfig struct {
// Endpoints etcd服务地址列表
Endpoints []string
// DialTimeout 连接超时时间
DialTimeout time.Duration
// Username 用户名(可选)
Username string
// Password 密码(可选)
Password string
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// EtcdClient 带重连功能的etcd客户端
type EtcdClient struct { type EtcdClient struct {
config EtcdClientConfig config clientv3.Config
client *clientv3.Client client *clientv3.Client
manager *ConnectionManager mu sync.RWMutex
mu sync.RWMutex
} }
// etcdConnector etcd连接器 // NewEtcdClient 创建新的etcd客户端
type etcdConnector struct { func NewEtcdClient(config clientv3.Config) *EtcdClient {
client *EtcdClient return &EtcdClient{
config: config,
}
} }
func (e *etcdConnector) Connect(ctx context.Context) error { // Connect 建立etcd连接
e.client.mu.Lock() func (e *EtcdClient) Connect(ctx context.Context) error {
defer e.client.mu.Unlock() e.mu.Lock()
defer e.mu.Unlock()
cfg := clientv3.Config{ if e.client != nil {
Endpoints: e.client.config.Endpoints, _ = e.client.Close()
DialTimeout: e.client.config.DialTimeout,
Username: e.client.config.Username,
Password: e.client.config.Password,
} }
if cfg.DialTimeout == 0 { client, err := clientv3.New(e.config)
cfg.DialTimeout = 5 * time.Second
}
cli, err := clientv3.New(cfg)
if err != nil { if err != nil {
return err return fmt.Errorf("etcd new client failed: %w", err)
} }
// 验证连接 // 测试连接
ctx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
_, err = cli.Status(ctx, cfg.Endpoints[0]) _, err = client.Status(timeoutCtx, e.config.Endpoints[0])
if err != nil { if err != nil {
cli.Close() _ = client.Close()
return err return fmt.Errorf("etcd status check failed: %w", err)
} }
e.client.client = cli e.client = client
return nil return nil
} }
func (e *etcdConnector) Close() error { // Close 关闭etcd连接
e.client.mu.Lock() func (e *EtcdClient) Close() error {
defer e.client.mu.Unlock() e.mu.Lock()
defer e.mu.Unlock()
if e.client.client != nil { if e.client != nil {
err := e.client.client.Close() err := e.client.Close()
e.client.client = nil e.client = nil
return err return err
} }
return nil return nil
} }
// etcdHealthChecker etcd健康检查器 // Ping 检查etcd连接是否健康
type etcdHealthChecker struct { func (e *EtcdClient) Ping(ctx context.Context) error {
client *EtcdClient e.mu.RLock()
} defer e.mu.RUnlock()
func (e *etcdHealthChecker) HealthCheck(ctx context.Context) error { if e.client == nil {
e.client.mu.RLock() return fmt.Errorf("etcd client is nil")
cli := e.client.client
endpoints := e.client.config.Endpoints
e.client.mu.RUnlock()
if cli == nil || len(endpoints) == 0 {
return context.Canceled
} }
_, err := cli.Status(ctx, endpoints[0]) if len(e.config.Endpoints) == 0 {
return fmt.Errorf("etcd endpoints is empty")
}
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, err := e.client.Status(timeoutCtx, e.config.Endpoints[0])
return err return err
} }
// NewEtcdClient 创建带重连功能的etcd客户端 // IsConnected 检查连接状态
func NewEtcdClient(cfg EtcdClientConfig) (*EtcdClient, error) { func (e *EtcdClient) IsConnected() bool {
client := &EtcdClient{ e.mu.RLock()
config: cfg, defer e.mu.RUnlock()
}
connector := &etcdConnector{client: client} return e.client != nil
checker := &etcdHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
} }
// GetClient 获取etcd客户端 // GetClient 获取底层的etcd客户端
func (c *EtcdClient) GetClient() *clientv3.Client { func (e *EtcdClient) GetClient() *clientv3.Client {
c.mu.RLock() e.mu.RLock()
defer c.mu.RUnlock() defer e.mu.RUnlock()
return c.client
return e.client
} }
// State 获取连接状态 // EtcdManager 创建etcd重连管理器
func (c *EtcdClient) State() ConnectionState { func EtcdManager(config clientv3.Config, reconnectConfig *Config) *Manager {
return c.manager.State() client := NewEtcdClient(config)
} return NewManager(client, reconnectConfig)
// Close 关闭客户端
func (c *EtcdClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *EtcdClient) TriggerReconnect() {
c.manager.TriggerReconnect()
} }
-338
View File
@@ -1,338 +0,0 @@
package reconnect
import (
"context"
"fmt"
"log"
"sync"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
// EtcdRegistryConfig etcd 服务注册配置
type EtcdRegistryConfig struct {
// Endpoints etcd 服务地址列表
Endpoints []string
// DialTimeout 连接超时时间
DialTimeout time.Duration
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// EtcdRegistry etcd 服务注册(带自动重连)
type EtcdRegistry struct {
config EtcdRegistryConfig
etcdClient *EtcdClient
cli *clientv3.Client
key string
val string
serviceName string
ttl int64
leaseID clientv3.LeaseID
keepAliveCh <-chan *clientv3.LeaseKeepAliveResponse
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
watcherCtx context.Context
watcherCancel context.CancelFunc
watcherMu sync.Mutex
lastRegisterTime time.Time
registerMu sync.Mutex
}
// NewEtcdRegistry 创建带重连功能的 etcd 服务注册器
func NewEtcdRegistry(cfg EtcdRegistryConfig) (*EtcdRegistry, error) {
ctx, cancel := context.WithCancel(context.Background())
// 创建带重连的 etcd 客户端
etcdCfg := EtcdClientConfig{
Endpoints: cfg.Endpoints,
DialTimeout: cfg.DialTimeout,
ReconnectConfig: cfg.ReconnectConfig,
}
etcdClient, err := NewEtcdClient(etcdCfg)
if err != nil {
cancel()
return nil, err
}
watcherCtx, watcherCancel := context.WithCancel(context.Background())
return &EtcdRegistry{
config: cfg,
etcdClient: etcdClient,
cli: etcdClient.GetClient(),
ctx: ctx,
cancel: cancel,
watcherCtx: watcherCtx,
watcherCancel: watcherCancel,
}, nil
}
// Register 注册服务
func (r *EtcdRegistry) Register(serviceName, addr string, ttl int64) error {
r.mu.Lock()
r.serviceName = serviceName
r.ttl = ttl
r.key = fmt.Sprintf("/%s/%s", serviceName, addr)
r.val = addr
r.mu.Unlock()
return r.registerWithKV(ttl)
}
func (r *EtcdRegistry) registerWithKV(ttl int64) error {
// 防抖:防止短时间内重复注册
r.registerMu.Lock()
now := time.Now()
if !r.lastRegisterTime.IsZero() && now.Sub(r.lastRegisterTime) < 2*time.Second {
r.registerMu.Unlock()
return fmt.Errorf("register too frequently, please wait")
}
r.lastRegisterTime = now
r.registerMu.Unlock()
r.mu.RLock()
cli := r.cli
key := r.key
val := r.val
r.mu.RUnlock()
// 如果 cli 为 nil 或连接已关闭,尝试重新获取客户端
if cli == nil {
if r.etcdClient != nil {
// 检查连接状态
state := r.etcdClient.State()
if state != StateConnected {
return fmt.Errorf("etcd client is not connected, current state: %s", state)
}
r.mu.Lock()
r.cli = r.etcdClient.GetClient()
cli = r.cli
r.mu.Unlock()
}
if cli == nil {
return fmt.Errorf("etcd client is nil")
}
} else {
// 即使 cli 不为 nil,也要检查连接状态,因为可能连接已断开但 cli 引用还在
if r.etcdClient != nil {
state := r.etcdClient.State()
if state != StateConnected {
// 连接已断开,尝试更新 cli 引用
r.mu.Lock()
r.cli = r.etcdClient.GetClient()
cli = r.cli
r.mu.Unlock()
if cli == nil {
return fmt.Errorf("etcd client is not connected, current state: %s", state)
}
}
}
}
// 使用带超时的 context 来避免长时间阻塞
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := cli.Grant(ctx, ttl)
if err != nil {
// 如果 Grant 失败,可能是连接已关闭,尝试更新 client 引用
if r.etcdClient != nil {
r.mu.Lock()
r.cli = r.etcdClient.GetClient()
cli = r.cli
r.mu.Unlock()
if cli != nil {
// 使用新的 client 重试
ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()
resp, err = cli.Grant(ctx2, ttl)
}
}
if err != nil {
return fmt.Errorf("failed to grant lease: %w", err)
}
}
r.mu.Lock()
r.leaseID = resp.ID
r.mu.Unlock()
ctx3, cancel3 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel3()
_, err = cli.Put(ctx3, key, val, clientv3.WithLease(resp.ID))
if err != nil {
return fmt.Errorf("failed to put key: %w", err)
}
// KeepAlive 需要使用长期有效的 context,不能使用带超时的 context
keepAliveCh, err := cli.KeepAlive(context.Background(), resp.ID)
if err != nil {
return fmt.Errorf("failed to start keep-alive: %w", err)
}
r.mu.Lock()
r.keepAliveCh = keepAliveCh
r.mu.Unlock()
// 停止旧的 watcher(如果存在)
r.watcherMu.Lock()
if r.watcherCancel != nil {
r.watcherCancel()
}
// 创建新的 watcher context
r.watcherCtx, r.watcherCancel = context.WithCancel(context.Background())
watcherCtx := r.watcherCtx
r.watcherMu.Unlock()
go r.watcher(ttl, watcherCtx)
log.Printf("[EtcdRegistry] 服务注册成功: %s", key)
return nil
}
// watcher 监听续约
func (r *EtcdRegistry) watcher(ttl int64, watcherCtx context.Context) {
for {
select {
case <-r.ctx.Done():
log.Println("[EtcdRegistry] context done, watcher exiting.")
return
case <-watcherCtx.Done():
log.Println("[EtcdRegistry] watcher context done, exiting.")
return
case ka, ok := <-r.keepAliveCh:
if !ok {
log.Println("[EtcdRegistry] keep-alive channel closed, waiting for connection recovery...")
// 等待连接恢复,而不是立即重试
maxWaitTime := 60 * time.Second
checkInterval := 500 * time.Millisecond
waited := time.Duration(0)
for waited < maxWaitTime {
select {
case <-r.ctx.Done():
return
case <-watcherCtx.Done():
return
default:
}
// 检查连接状态
if r.etcdClient != nil {
state := r.etcdClient.State()
if state == StateConnected {
// 连接已恢复,更新 cli 引用
r.mu.Lock()
r.cli = r.etcdClient.GetClient()
r.mu.Unlock()
// 等待一段时间确保连接稳定,避免立即重新注册
time.Sleep(1 * time.Second)
// 尝试重新注册
if err := r.registerWithKV(ttl); err == nil {
log.Println("[EtcdRegistry] 服务重新注册成功")
return // 退出当前 goroutine,让新的 watcher 启动
} else {
// 如果是防抖错误,继续等待
if err.Error() == "register too frequently, please wait" {
log.Println("[EtcdRegistry] 注册过于频繁,继续等待...")
time.Sleep(2 * time.Second)
} else {
log.Printf("[EtcdRegistry] 重新注册失败: %v, 继续等待连接恢复...", err)
}
}
}
}
time.Sleep(checkInterval)
waited += checkInterval
}
// 如果等待超时,使用指数退避重试
log.Println("[EtcdRegistry] 等待连接恢复超时,使用重试机制...")
delay := 2 * time.Second
maxDelay := 30 * time.Second
for {
select {
case <-r.ctx.Done():
return
case <-watcherCtx.Done():
return
default:
}
// 再次检查连接状态
if r.etcdClient != nil {
state := r.etcdClient.State()
if state == StateConnected {
r.mu.Lock()
r.cli = r.etcdClient.GetClient()
r.mu.Unlock()
}
}
if err := r.registerWithKV(ttl); err != nil {
// 如果是防抖错误,等待更长时间
if err.Error() == "register too frequently, please wait" {
log.Println("[EtcdRegistry] 注册过于频繁,等待更长时间...")
time.Sleep(3 * time.Second)
continue
}
log.Printf("[EtcdRegistry] failed to re-register service: %v, retrying in %v...", err, delay)
time.Sleep(delay)
delay *= 2
if delay > maxDelay {
delay = maxDelay
}
} else {
log.Println("[EtcdRegistry] 服务重新注册成功")
return // 退出当前 goroutine,让新的 watcher 启动
}
}
}
// 续约成功
_ = ka
}
}
}
// UnRegister 注销服务
func (r *EtcdRegistry) UnRegister() {
r.cancel() // 停止主 context
// 停止 watcher
r.watcherMu.Lock()
if r.watcherCancel != nil {
r.watcherCancel()
}
r.watcherMu.Unlock()
r.mu.RLock()
cli := r.cli
leaseID := r.leaseID
key := r.key
r.mu.RUnlock()
if cli != nil {
cli.Revoke(context.Background(), leaseID)
cli.Delete(context.Background(), key)
log.Printf("[EtcdRegistry] 服务注销成功: %s", key)
}
if r.etcdClient != nil {
r.etcdClient.Close()
}
}
// State 获取连接状态
func (r *EtcdRegistry) State() ConnectionState {
if r.etcdClient != nil {
return r.etcdClient.State()
}
return StateDisconnected
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"context"
"fmt"
"log"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"github.com/blueocean-go/reconnect"
)
func main() {
// etcd配置
etcdConfig := clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
}
// 创建重连配置
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.MaxRetries = 0 // 无限重试
config.OnReconnect = func() {
fmt.Println("etcd重连成功!")
}
config.OnDisconnect = func(err error) {
fmt.Printf("etcd连接断开: %v\n", err)
}
// 创建重连管理器
manager := reconnect.EtcdManager(etcdConfig, config)
// 启动连接
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatalf("启动失败: %v", err)
}
defer manager.Stop()
// 获取etcd客户端并使用
etcdClient := reconnect.NewEtcdClient(etcdConfig)
if err := etcdClient.Connect(ctx); err != nil {
log.Fatalf("连接失败: %v", err)
}
client := etcdClient.GetClient()
if client == nil {
log.Fatal("客户端为空")
}
// 使用etcd客户端
kv := clientv3.NewKV(client)
_, err := kv.Put(ctx, "key", "value")
if err != nil {
log.Printf("设置键值失败: %v", err)
} else {
fmt.Println("设置键值成功")
}
resp, err := kv.Get(ctx, "key")
if err != nil {
log.Printf("获取键值失败: %v", err)
} else {
if len(resp.Kvs) > 0 {
fmt.Printf("获取键值: %s = %s\n", resp.Kvs[0].Key, resp.Kvs[0].Value)
}
}
// 保持运行
select {}
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/blueocean-go/reconnect"
)
func main() {
// PostgreSQL连接字符串
dsn := "host=localhost user=postgres password=postgres dbname=testdb sslmode=disable"
// 创建重连配置
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.MaxRetries = 0 // 无限重试
config.OnReconnect = func() {
fmt.Println("PostgreSQL重连成功!")
}
config.OnDisconnect = func(err error) {
fmt.Printf("PostgreSQL连接断开: %v\n", err)
}
// 创建重连管理器
manager := reconnect.PostgresManager(
dsn,
25, // maxConnections
5, // maxIdleConnections
30*time.Minute, // maxConnectionAge
config,
)
// 启动连接
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatalf("启动失败: %v", err)
}
defer manager.Stop()
// 获取数据库连接并使用
pgClient := reconnect.NewPostgresClient(dsn, 25, 5, 30*time.Minute)
if err := pgClient.Connect(ctx); err != nil {
log.Fatalf("连接失败: %v", err)
}
db := pgClient.GetDB()
if db == nil {
log.Fatal("数据库连接为空")
}
// 使用数据库连接
var version string
err := db.QueryRow("SELECT version()").Scan(&version)
if err != nil {
log.Printf("查询失败: %v", err)
} else {
fmt.Printf("PostgreSQL版本: %s\n", version)
}
// 保持运行
select {}
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/blueocean-go/reconnect"
"github.com/redis/go-redis/v9"
)
func main() {
// 创建Redis选项
options := &redis.Options{
Addr: "localhost:6379",
Password: "", // 无密码
DB: 0,
}
// 创建重连配置
config := reconnect.DefaultConfig()
config.RetryInterval = 3 * time.Second
config.MaxRetries = 0 // 无限重试
config.OnReconnect = func() {
fmt.Println("Redis重连成功!")
}
config.OnDisconnect = func(err error) {
fmt.Printf("Redis连接断开: %v\n", err)
}
// 创建重连管理器
manager := reconnect.RedisManager(options, config)
// 启动连接
ctx := context.Background()
if err := manager.Start(ctx); err != nil {
log.Fatalf("启动失败: %v", err)
}
defer manager.Stop()
// 获取Redis客户端并使用
redisClient := reconnect.NewRedisClient(options)
if err := redisClient.Connect(ctx); err != nil {
log.Fatalf("连接失败: %v", err)
}
client := redisClient.GetClient()
if client == nil {
log.Fatal("客户端为空")
}
// 使用Redis客户端
err := client.Set(ctx, "key", "value", 0).Err()
if err != nil {
log.Printf("设置键值失败: %v", err)
} else {
fmt.Println("设置键值成功")
}
val, err := client.Get(ctx, "key").Result()
if err != nil {
log.Printf("获取键值失败: %v", err)
} else {
fmt.Printf("获取键值: %s\n", val)
}
// 保持运行
select {}
}
+22 -24
View File
@@ -1,33 +1,31 @@
module git.whblueocean.cn/blueocean-go/reconnect module github.com/blueocean-go/reconnect
go 1.24.0 go 1.21
require ( require (
github.com/go-redis/redis/v8 v8.11.5 github.com/lib/pq v1.10.9
github.com/rabbitmq/amqp091-go v1.10.0 github.com/redis/go-redis/v9 v9.3.0
go.etcd.io/etcd/client/v3 v3.6.5 go.etcd.io/etcd/client/v3 v3.5.10
google.golang.org/grpc v1.75.1
gorm.io/gorm v1.31.1
) )
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect go.etcd.io/etcd/api/v3 v3.5.10 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
github.com/jinzhu/now v1.1.5 // indirect go.uber.org/atomic v1.7.0 // indirect
go.etcd.io/etcd/api/v3 v3.6.5 // indirect go.uber.org/multierr v1.6.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect go.uber.org/zap v1.17.0 // indirect
go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.17.0 // indirect
go.uber.org/zap v1.27.0 // indirect golang.org/x/sys v0.13.0 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/text v0.13.0 // indirect
golang.org/x/sys v0.33.0 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
golang.org/x/text v0.26.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.31.0 // indirect
) )
+61 -80
View File
@@ -1,76 +1,56 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k=
go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8= go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0=
go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao=
go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -80,20 +60,20 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -102,22 +82,23 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
-162
View File
@@ -1,162 +0,0 @@
package reconnect
import (
"context"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials/insecure"
)
// GRPCClientConfig gRPC客户端配置
type GRPCClientConfig struct {
// Target gRPC服务地址
Target string
// DialOptions gRPC拨号选项
DialOptions []grpc.DialOption
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// GRPCClient 带重连功能的gRPC客户端
type GRPCClient struct {
config GRPCClientConfig
conn *grpc.ClientConn
manager *ConnectionManager
mu sync.RWMutex
}
// grpcConnector gRPC连接器
type grpcConnector struct {
client *GRPCClient
}
func (g *grpcConnector) Connect(ctx context.Context) error {
g.client.mu.Lock()
defer g.client.mu.Unlock()
opts := g.client.config.DialOptions
if len(opts) == 0 {
opts = []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
}
conn, err := grpc.NewClient(g.client.config.Target, opts...)
if err != nil {
return err
}
// 等待连接就绪
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
conn.Connect()
for {
state := conn.GetState()
if state == connectivity.Ready {
break
}
if state == connectivity.TransientFailure || state == connectivity.Shutdown {
conn.Close()
return context.DeadlineExceeded
}
if !conn.WaitForStateChange(ctx, state) {
conn.Close()
return ctx.Err()
}
}
g.client.conn = conn
return nil
}
func (g *grpcConnector) Close() error {
g.client.mu.Lock()
defer g.client.mu.Unlock()
if g.client.conn != nil {
err := g.client.conn.Close()
g.client.conn = nil
return err
}
return nil
}
// grpcHealthChecker gRPC健康检查器
type grpcHealthChecker struct {
client *GRPCClient
}
func (g *grpcHealthChecker) HealthCheck(ctx context.Context) error {
g.client.mu.RLock()
conn := g.client.conn
g.client.mu.RUnlock()
if conn == nil {
return context.Canceled
}
state := conn.GetState()
if state == connectivity.Ready || state == connectivity.Idle {
return nil
}
// 尝试连接并等待
conn.Connect()
if !conn.WaitForStateChange(ctx, state) {
return ctx.Err()
}
newState := conn.GetState()
if newState != connectivity.Ready && newState != connectivity.Idle {
return context.DeadlineExceeded
}
return nil
}
// NewGRPCClient 创建带重连功能的gRPC客户端
func NewGRPCClient(cfg GRPCClientConfig) (*GRPCClient, error) {
client := &GRPCClient{
config: cfg,
}
connector := &grpcConnector{client: client}
checker := &grpcHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
}
// GetConn 获取gRPC连接
func (c *GRPCClient) GetConn() *grpc.ClientConn {
c.mu.RLock()
defer c.mu.RUnlock()
return c.conn
}
// State 获取连接状态
func (c *GRPCClient) State() ConnectionState {
return c.manager.State()
}
// Close 关闭客户端
func (c *GRPCClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *GRPCClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
-274
View File
@@ -1,274 +0,0 @@
package reconnect
import (
"context"
"log"
"sync"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
)
// GRPCEtcdClientConfig 使用 etcd 服务发现的 gRPC 客户端配置
type GRPCEtcdClientConfig struct {
// EtcdEndpoints etcd 服务地址列表
EtcdEndpoints []string
// EtcdDialTimeout etcd 连接超时时间
EtcdDialTimeout time.Duration
// ServiceName 服务名称(在 etcd 中注册的 key 前缀)
ServiceName string
// DialOptions 额外的 gRPC 拨号选项
DialOptions []grpc.DialOption
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// GRPCEtcdClient 带 etcd 服务发现和重连功能的 gRPC 客户端
type GRPCEtcdClient struct {
config GRPCEtcdClientConfig
etcdClient *clientv3.Client
conn *grpc.ClientConn
manager *ConnectionManager
mu sync.RWMutex
}
// grpcEtcdConnector gRPC + etcd 连接器
type grpcEtcdConnector struct {
client *GRPCEtcdClient
}
func (g *grpcEtcdConnector) Connect(ctx context.Context) error {
g.client.mu.Lock()
defer g.client.mu.Unlock()
cfg := g.client.config
// 创建 etcd 客户端
etcdTimeout := cfg.EtcdDialTimeout
if etcdTimeout == 0 {
etcdTimeout = 5 * time.Second
}
cli, err := clientv3.New(clientv3.Config{
Endpoints: cfg.EtcdEndpoints,
DialTimeout: etcdTimeout,
})
if err != nil {
return err
}
// 注册 etcd resolver
etcdResolver := &etcdResolverBuilder{cli: cli}
resolver.Register(etcdResolver)
target := "etcd:///" + cfg.ServiceName + "/"
// 默认 DialOptions
opts := []grpc.DialOption{
grpc.WithResolvers(etcdResolver),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
}
// 追加用户自定义选项
opts = append(opts, cfg.DialOptions...)
conn, err := grpc.NewClient(target, opts...)
if err != nil {
cli.Close()
return err
}
// 等待连接就绪
connCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
conn.Connect()
for {
state := conn.GetState()
if state == connectivity.Ready {
break
}
if state == connectivity.TransientFailure || state == connectivity.Shutdown {
conn.Close()
cli.Close()
return context.DeadlineExceeded
}
if !conn.WaitForStateChange(connCtx, state) {
conn.Close()
cli.Close()
return connCtx.Err()
}
}
g.client.etcdClient = cli
g.client.conn = conn
return nil
}
func (g *grpcEtcdConnector) Close() error {
g.client.mu.Lock()
defer g.client.mu.Unlock()
if g.client.conn != nil {
g.client.conn.Close()
g.client.conn = nil
}
if g.client.etcdClient != nil {
g.client.etcdClient.Close()
g.client.etcdClient = nil
}
return nil
}
// grpcEtcdHealthChecker gRPC + etcd 健康检查器
type grpcEtcdHealthChecker struct {
client *GRPCEtcdClient
}
func (g *grpcEtcdHealthChecker) HealthCheck(ctx context.Context) error {
g.client.mu.RLock()
conn := g.client.conn
g.client.mu.RUnlock()
if conn == nil {
return context.Canceled
}
state := conn.GetState()
if state == connectivity.Ready || state == connectivity.Idle {
return nil
}
conn.Connect()
if !conn.WaitForStateChange(ctx, state) {
return ctx.Err()
}
newState := conn.GetState()
if newState != connectivity.Ready && newState != connectivity.Idle {
return context.DeadlineExceeded
}
return nil
}
// NewGRPCClientWithEtcd 创建带 etcd 服务发现和重连功能的 gRPC 客户端
func NewGRPCClientWithEtcd(cfg GRPCEtcdClientConfig) (*GRPCEtcdClient, error) {
client := &GRPCEtcdClient{
config: cfg,
}
connector := &grpcEtcdConnector{client: client}
checker := &grpcEtcdHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
}
// GetConn 获取 gRPC 连接
func (c *GRPCEtcdClient) GetConn() *grpc.ClientConn {
c.mu.RLock()
defer c.mu.RUnlock()
return c.conn
}
// GetEtcdClient 获取 etcd 客户端
func (c *GRPCEtcdClient) GetEtcdClient() *clientv3.Client {
c.mu.RLock()
defer c.mu.RUnlock()
return c.etcdClient
}
// State 获取连接状态
func (c *GRPCEtcdClient) State() ConnectionState {
return c.manager.State()
}
// Close 关闭客户端
func (c *GRPCEtcdClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *GRPCEtcdClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
// ================== etcd resolver 实现 ==================
// etcdResolverBuilder 实现 resolver.Builder
type etcdResolverBuilder struct {
cli *clientv3.Client
}
func (b *etcdResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &etcdResolver{
cli: b.cli,
cc: cc,
target: target,
}
go r.watch(target.Endpoint())
return r, nil
}
func (b *etcdResolverBuilder) Scheme() string {
return "etcd"
}
// etcdResolver 实现 resolver.Resolver
type etcdResolver struct {
cli *clientv3.Client
cc resolver.ClientConn
target resolver.Target
}
func (r *etcdResolver) watch(keyPrefix string) {
// 首次获取服务列表
resp, err := r.cli.Get(context.Background(), "/"+keyPrefix, clientv3.WithPrefix())
if err != nil {
log.Printf("[etcdResolver] 获取服务列表失败: %v", err)
return
}
addrs := make([]resolver.Address, 0, len(resp.Kvs))
for _, kv := range resp.Kvs {
addrs = append(addrs, resolver.Address{Addr: string(kv.Value)})
}
r.cc.UpdateState(resolver.State{Addresses: addrs})
// 监听变化
watchCh := r.cli.Watch(context.Background(), "/"+keyPrefix, clientv3.WithPrefix())
for wresp := range watchCh {
for _, ev := range wresp.Events {
switch ev.Type {
case clientv3.EventTypePut:
addrs = append(addrs, resolver.Address{Addr: string(ev.Kv.Value)})
case clientv3.EventTypeDelete:
newAddrs := make([]resolver.Address, 0)
for _, addr := range addrs {
if addr.Addr != string(ev.Kv.Value) {
newAddrs = append(newAddrs, addr)
}
}
addrs = newAddrs
}
r.cc.UpdateState(resolver.State{Addresses: addrs})
}
}
}
func (r *etcdResolver) ResolveNow(opts resolver.ResolveNowOptions) {}
func (r *etcdResolver) Close() {}
-225
View File
@@ -1,225 +0,0 @@
package reconnect
import (
"context"
"log"
"sync"
"sync/atomic"
"time"
)
// HealthChecker 健康检查接口
type HealthChecker interface {
// HealthCheck 执行健康检查,返回 nil 表示健康
HealthCheck(ctx context.Context) error
}
// Connector 连接器接口
type Connector interface {
// Connect 建立连接
Connect(ctx context.Context) error
// Close 关闭连接
Close() error
}
// ConnectionManager 连接管理器
type ConnectionManager struct {
config Config
strategy Strategy
connector Connector
checker HealthChecker
state atomic.Int32
attempts atomic.Int32
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
closeOnce sync.Once
}
// NewConnectionManager 创建连接管理器
func NewConnectionManager(connector Connector, checker HealthChecker, cfg Config) *ConnectionManager {
cfg.Validate()
ctx, cancel := context.WithCancel(context.Background())
cm := &ConnectionManager{
config: cfg,
strategy: NewStrategy(cfg),
connector: connector,
checker: checker,
ctx: ctx,
cancel: cancel,
}
cm.state.Store(int32(StateDisconnected))
return cm
}
// State 获取当前连接状态
func (cm *ConnectionManager) State() ConnectionState {
return ConnectionState(cm.state.Load())
}
// setState 设置连接状态
func (cm *ConnectionManager) setState(newState ConnectionState) {
oldState := ConnectionState(cm.state.Swap(int32(newState)))
if oldState != newState && cm.config.OnStateChange != nil {
cm.config.OnStateChange(oldState, newState)
}
}
// Connect 建立连接并启动健康检查
func (cm *ConnectionManager) Connect(ctx context.Context) error {
cm.setState(StateConnecting)
if err := cm.connector.Connect(ctx); err != nil {
cm.setState(StateDisconnected)
if cm.config.OnError != nil {
cm.config.OnError(err)
}
return err
}
cm.setState(StateConnected)
cm.strategy.Reset()
cm.attempts.Store(0)
// 启动健康检查
if cm.checker != nil && cm.config.HealthCheckInterval > 0 {
cm.wg.Add(1)
go cm.healthCheckLoop()
}
return nil
}
// ConnectWithRetry 带重试的连接
func (cm *ConnectionManager) ConnectWithRetry(ctx context.Context) error {
cm.setState(StateConnecting)
for {
select {
case <-ctx.Done():
cm.setState(StateDisconnected)
return ctx.Err()
case <-cm.ctx.Done():
cm.setState(StateDisconnected)
return cm.ctx.Err()
default:
}
attempt := int(cm.attempts.Add(1))
if err := cm.connector.Connect(ctx); err != nil {
if cm.config.OnError != nil {
cm.config.OnError(err)
}
// 检查是否超过最大重试次数
if cm.config.MaxRetries >= 0 && attempt >= cm.config.MaxRetries {
cm.setState(StateDisconnected)
return err
}
delay := cm.strategy.NextDelay(attempt)
log.Printf("[reconnect] 连接失败 (尝试 %d): %v, %v 后重试...", attempt, err, delay)
select {
case <-ctx.Done():
cm.setState(StateDisconnected)
return ctx.Err()
case <-cm.ctx.Done():
cm.setState(StateDisconnected)
return cm.ctx.Err()
case <-time.After(delay):
continue
}
}
// 连接成功
cm.setState(StateConnected)
cm.strategy.Reset()
cm.attempts.Store(0)
if cm.config.OnReconnect != nil && attempt > 1 {
cm.config.OnReconnect(attempt)
}
// 启动健康检查
if cm.checker != nil && cm.config.HealthCheckInterval > 0 {
cm.wg.Add(1)
go cm.healthCheckLoop()
}
log.Printf("[reconnect] 连接成功")
return nil
}
}
// healthCheckLoop 健康检查循环
func (cm *ConnectionManager) healthCheckLoop() {
defer cm.wg.Done()
ticker := time.NewTicker(cm.config.HealthCheckInterval)
defer ticker.Stop()
for {
select {
case <-cm.ctx.Done():
return
case <-ticker.C:
if cm.State() != StateConnected {
continue
}
ctx, cancel := context.WithTimeout(cm.ctx, cm.config.HealthCheckTimeout)
err := cm.checker.HealthCheck(ctx)
cancel()
if err != nil {
log.Printf("[reconnect] 健康检查失败: %v, 开始重连...", err)
if cm.config.OnError != nil {
cm.config.OnError(err)
}
cm.reconnect()
}
}
}
}
// reconnect 执行重连
func (cm *ConnectionManager) reconnect() {
if cm.State() == StateReconnecting {
return // 已经在重连中
}
cm.setState(StateReconnecting)
// 关闭旧连接
_ = cm.connector.Close()
// 重新连接
go func() {
if err := cm.ConnectWithRetry(cm.ctx); err != nil {
log.Printf("[reconnect] 重连失败: %v", err)
}
}()
}
// Close 关闭连接管理器
func (cm *ConnectionManager) Close() error {
var err error
cm.closeOnce.Do(func() {
cm.cancel()
cm.wg.Wait()
err = cm.connector.Close()
cm.setState(StateDisconnected)
})
return err
}
// TriggerReconnect 手动触发重连
func (cm *ConnectionManager) TriggerReconnect() {
cm.reconnect()
}
+106
View File
@@ -0,0 +1,106 @@
package reconnect
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
_ "github.com/lib/pq"
)
// PostgresClient PostgreSQL客户端包装器
type PostgresClient struct {
dsn string
db *sql.DB
mu sync.RWMutex
maxAge time.Duration
}
// NewPostgresClient 创建新的PostgreSQL客户端
func NewPostgresClient(dsn string, maxConnections int, maxIdleConnections int, maxConnectionAge time.Duration) *PostgresClient {
return &PostgresClient{
dsn: dsn,
maxAge: maxConnectionAge,
}
}
// Connect 建立PostgreSQL连接
func (p *PostgresClient) Connect(ctx context.Context) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.db != nil {
_ = p.db.Close()
}
db, err := sql.Open("postgres", p.dsn)
if err != nil {
return fmt.Errorf("postgres open failed: %w", err)
}
// 设置连接池参数
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
if p.maxAge > 0 {
db.SetConnMaxLifetime(p.maxAge)
}
// 测试连接
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return fmt.Errorf("postgres ping failed: %w", err)
}
p.db = db
return nil
}
// Close 关闭PostgreSQL连接
func (p *PostgresClient) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.db != nil {
err := p.db.Close()
p.db = nil
return err
}
return nil
}
// Ping 检查PostgreSQL连接是否健康
func (p *PostgresClient) Ping(ctx context.Context) error {
p.mu.RLock()
defer p.mu.RUnlock()
if p.db == nil {
return fmt.Errorf("postgres db is nil")
}
return p.db.PingContext(ctx)
}
// IsConnected 检查连接状态
func (p *PostgresClient) IsConnected() bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.db != nil
}
// GetDB 获取底层的数据库连接
func (p *PostgresClient) GetDB() *sql.DB {
p.mu.RLock()
defer p.mu.RUnlock()
return p.db
}
// PostgresManager 创建PostgreSQL重连管理器
func PostgresManager(dsn string, maxConnections int, maxIdleConnections int, maxConnectionAge time.Duration, config *Config) *Manager {
client := NewPostgresClient(dsn, maxConnections, maxIdleConnections, maxConnectionAge)
return NewManager(client, config)
}
-205
View File
@@ -1,205 +0,0 @@
package reconnect
import (
"context"
"sync"
amqp "github.com/rabbitmq/amqp091-go"
)
// RabbitMQClientConfig RabbitMQ客户端配置
type RabbitMQClientConfig struct {
// URL RabbitMQ连接URL
URL string
// Config amqp配置
Config *amqp.Config
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// RabbitMQClient 带重连功能的RabbitMQ客户端
type RabbitMQClient struct {
config RabbitMQClientConfig
conn *amqp.Connection
manager *ConnectionManager
mu sync.RWMutex
}
// rabbitmqConnector RabbitMQ连接器
type rabbitmqConnector struct {
client *RabbitMQClient
}
func (r *rabbitmqConnector) Connect(ctx context.Context) error {
r.client.mu.Lock()
defer r.client.mu.Unlock()
var conn *amqp.Connection
var err error
if r.client.config.Config != nil {
conn, err = amqp.DialConfig(r.client.config.URL, *r.client.config.Config)
} else {
conn, err = amqp.Dial(r.client.config.URL)
}
if err != nil {
return err
}
// 验证连接
if conn.IsClosed() {
conn.Close()
return amqp.ErrClosed
}
r.client.conn = conn
return nil
}
func (r *rabbitmqConnector) Close() error {
r.client.mu.Lock()
defer r.client.mu.Unlock()
if r.client.conn != nil && !r.client.conn.IsClosed() {
err := r.client.conn.Close()
r.client.conn = nil
return err
}
return nil
}
// rabbitmqHealthChecker RabbitMQ健康检查器
type rabbitmqHealthChecker struct {
client *RabbitMQClient
}
func (r *rabbitmqHealthChecker) HealthCheck(ctx context.Context) error {
r.client.mu.RLock()
conn := r.client.conn
r.client.mu.RUnlock()
if conn == nil || conn.IsClosed() {
return amqp.ErrClosed
}
// 尝试创建通道来验证连接
ch, err := conn.Channel()
if err != nil {
return err
}
defer ch.Close()
return nil
}
// NewRabbitMQClient 创建带重连功能的RabbitMQ客户端
func NewRabbitMQClient(cfg RabbitMQClientConfig) (*RabbitMQClient, error) {
client := &RabbitMQClient{
config: cfg,
}
connector := &rabbitmqConnector{client: client}
checker := &rabbitmqHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
}
// GetConn 获取RabbitMQ连接
func (c *RabbitMQClient) GetConn() *amqp.Connection {
c.mu.RLock()
defer c.mu.RUnlock()
return c.conn
}
// Channel 创建通道
func (c *RabbitMQClient) Channel() (*amqp.Channel, error) {
conn := c.GetConn()
if conn == nil || conn.IsClosed() {
return nil, amqp.ErrClosed
}
return conn.Channel()
}
// State 获取连接状态
func (c *RabbitMQClient) State() ConnectionState {
return c.manager.State()
}
// Close 关闭客户端
func (c *RabbitMQClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *RabbitMQClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
// IsClosed 检查连接是否关闭
func (c *RabbitMQClient) IsClosed() bool {
conn := c.GetConn()
return conn == nil || conn.IsClosed()
}
// Publish 发布消息(带重连支持)
func (c *RabbitMQClient) Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error {
ch, err := c.Channel()
if err != nil {
return err
}
defer ch.Close()
return ch.Publish(exchange, key, mandatory, immediate, msg)
}
// Consume 消费消息(带重连支持)
func (c *RabbitMQClient) Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args amqp.Table) (<-chan amqp.Delivery, error) {
ch, err := c.Channel()
if err != nil {
return nil, err
}
return ch.Consume(queue, consumer, autoAck, exclusive, noLocal, noWait, args)
}
// QueueDeclare 声明队列(带重连支持)
func (c *RabbitMQClient) QueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) (amqp.Queue, error) {
ch, err := c.Channel()
if err != nil {
return amqp.Queue{}, err
}
defer ch.Close()
return ch.QueueDeclare(name, durable, autoDelete, exclusive, noWait, args)
}
// ExchangeDeclare 声明交换机(带重连支持)
func (c *RabbitMQClient) ExchangeDeclare(name, kind string, durable, autoDelete, internal, noWait bool, args amqp.Table) error {
ch, err := c.Channel()
if err != nil {
return err
}
defer ch.Close()
return ch.ExchangeDeclare(name, kind, durable, autoDelete, internal, noWait, args)
}
// QueueBind 绑定队列到交换机(带重连支持)
func (c *RabbitMQClient) QueueBind(name, key, exchange string, noWait bool, args amqp.Table) error {
ch, err := c.Channel()
if err != nil {
return err
}
defer ch.Close()
return ch.QueueBind(name, key, exchange, noWait, args)
}
+162
View File
@@ -0,0 +1,162 @@
package reconnect
import (
"context"
"errors"
"fmt"
"time"
)
// Reconnectable 定义可重连的接口
type Reconnectable interface {
// Connect 建立连接
Connect(ctx context.Context) error
// Close 关闭连接
Close() error
// Ping 检查连接是否健康
Ping(ctx context.Context) error
// IsConnected 检查连接状态
IsConnected() bool
}
// Config 重连配置
type Config struct {
// MaxRetries 最大重试次数,0表示无限重试
MaxRetries int
// RetryInterval 重试间隔
RetryInterval time.Duration
// Timeout 连接超时时间
Timeout time.Duration
// OnReconnect 重连成功后的回调函数
OnReconnect func()
// OnDisconnect 断开连接时的回调函数
OnDisconnect func(error)
}
// DefaultConfig 返回默认配置
func DefaultConfig() *Config {
return &Config{
MaxRetries: 0, // 无限重试
RetryInterval: 3 * time.Second,
Timeout: 10 * time.Second,
}
}
// Manager 重连管理器
type Manager struct {
client Reconnectable
config *Config
ctx context.Context
cancel context.CancelFunc
connected bool
errCh chan error
}
// NewManager 创建新的重连管理器
func NewManager(client Reconnectable, config *Config) *Manager {
if config == nil {
config = DefaultConfig()
}
ctx, cancel := context.WithCancel(context.Background())
return &Manager{
client: client,
config: config,
ctx: ctx,
cancel: cancel,
errCh: make(chan error, 1),
}
}
// Start 启动连接并开始监控
func (m *Manager) Start(ctx context.Context) error {
// 初始连接
if err := m.connect(ctx); err != nil {
return fmt.Errorf("initial connection failed: %w", err)
}
// 启动监控 goroutine
go m.monitor()
return nil
}
// Stop 停止重连管理器
func (m *Manager) Stop() error {
m.cancel()
if m.client != nil {
return m.client.Close()
}
return nil
}
// connect 执行连接
func (m *Manager) connect(ctx context.Context) error {
connectCtx, cancel := context.WithTimeout(ctx, m.config.Timeout)
defer cancel()
if err := m.client.Connect(connectCtx); err != nil {
return err
}
m.connected = true
if m.config.OnReconnect != nil {
m.config.OnReconnect()
}
return nil
}
// monitor 监控连接状态并自动重连
func (m *Manager) monitor() {
ticker := time.NewTicker(m.config.RetryInterval)
defer ticker.Stop()
retryCount := 0
for {
select {
case <-m.ctx.Done():
return
case <-ticker.C:
// 检查连接状态
if !m.client.IsConnected() {
m.connected = false
if m.config.OnDisconnect != nil {
m.config.OnDisconnect(errors.New("connection lost"))
}
// 检查是否超过最大重试次数
if m.config.MaxRetries > 0 && retryCount >= m.config.MaxRetries {
m.errCh <- fmt.Errorf("max retries (%d) exceeded", m.config.MaxRetries)
return
}
// 尝试重连
if err := m.connect(context.Background()); err != nil {
retryCount++
continue
}
// 重连成功,重置计数器
retryCount = 0
} else {
// 连接正常,执行健康检查
if err := m.client.Ping(context.Background()); err != nil {
m.connected = false
if m.config.OnDisconnect != nil {
m.config.OnDisconnect(err)
}
}
}
}
}
}
// IsConnected 返回当前连接状态
func (m *Manager) IsConnected() bool {
return m.connected && m.client.IsConnected()
}
// WaitForError 等待错误(用于阻塞等待)
func (m *Manager) WaitForError() error {
return <-m.errCh
}
+50 -88
View File
@@ -2,125 +2,87 @@ package reconnect
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"github.com/go-redis/redis/v8" "github.com/redis/go-redis/v9"
) )
// RedisClientConfig Redis客户端配置 // RedisClient Redis客户端包装器
type RedisClientConfig struct {
// Options Redis连接选项
Options *redis.Options
// ReconnectConfig 重连配置
ReconnectConfig Config
}
// RedisClient 带重连功能的Redis客户端
type RedisClient struct { type RedisClient struct {
config RedisClientConfig options *redis.Options
client *redis.Client client *redis.Client
manager *ConnectionManager
mu sync.RWMutex mu sync.RWMutex
} }
// redisConnector Redis连接器 // NewRedisClient 创建新的Redis客户端
type redisConnector struct { func NewRedisClient(options *redis.Options) *RedisClient {
client *RedisClient return &RedisClient{
options: options,
}
} }
func (r *redisConnector) Connect(ctx context.Context) error { // Connect 建立Redis连接
r.client.mu.Lock() func (r *RedisClient) Connect(ctx context.Context) error {
defer r.client.mu.Unlock() r.mu.Lock()
defer r.mu.Unlock()
rdb := redis.NewClient(r.client.config.Options) if r.client != nil {
_ = r.client.Close()
// 验证连接
if err := rdb.Ping(ctx).Err(); err != nil {
rdb.Close()
return err
} }
r.client.client = rdb client := redis.NewClient(r.options)
if err := client.Ping(ctx).Err(); err != nil {
return fmt.Errorf("redis ping failed: %w", err)
}
r.client = client
return nil return nil
} }
func (r *redisConnector) Close() error { // Close 关闭Redis连接
r.client.mu.Lock() func (r *RedisClient) Close() error {
defer r.client.mu.Unlock() r.mu.Lock()
defer r.mu.Unlock()
if r.client.client != nil { if r.client != nil {
err := r.client.client.Close() err := r.client.Close()
r.client.client = nil r.client = nil
return err return err
} }
return nil return nil
} }
// redisHealthChecker Redis健康检查器 // Ping 检查Redis连接是否健康
type redisHealthChecker struct { func (r *RedisClient) Ping(ctx context.Context) error {
client *RedisClient r.mu.RLock()
} defer r.mu.RUnlock()
func (r *redisHealthChecker) HealthCheck(ctx context.Context) error { if r.client == nil {
r.client.mu.RLock() return fmt.Errorf("redis client is nil")
rdb := r.client.client
r.client.mu.RUnlock()
if rdb == nil {
return context.Canceled
} }
return rdb.Ping(ctx).Err() return r.client.Ping(ctx).Err()
} }
// NewRedisClient 创建带重连功能的Redis客户端 // IsConnected 检查连接状态
func NewRedisClient(cfg RedisClientConfig) (*RedisClient, error) { func (r *RedisClient) IsConnected() bool {
client := &RedisClient{ r.mu.RLock()
config: cfg, defer r.mu.RUnlock()
}
connector := &redisConnector{client: client} return r.client != nil
checker := &redisHealthChecker{client: client}
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
// 首次连接
ctx := context.Background()
if err := client.manager.ConnectWithRetry(ctx); err != nil {
return nil, err
}
return client, nil
} }
// GetClient 获取Redis客户端 // GetClient 获取底层的Redis客户端
func (c *RedisClient) GetClient() *redis.Client { func (r *RedisClient) GetClient() *redis.Client {
c.mu.RLock() r.mu.RLock()
defer c.mu.RUnlock() defer r.mu.RUnlock()
return c.client
return r.client
} }
// State 获取连接状态 // RedisManager 创建Redis重连管理器
func (c *RedisClient) State() ConnectionState { func RedisManager(options *redis.Options, config *Config) *Manager {
return c.manager.State() client := NewRedisClient(options)
return NewManager(client, config)
} }
// Close 关闭客户端
func (c *RedisClient) Close() error {
return c.manager.Close()
}
// TriggerReconnect 手动触发重连
func (c *RedisClient) TriggerReconnect() {
c.manager.TriggerReconnect()
}
// Ping 执行Ping命令
func (c *RedisClient) Ping(ctx context.Context) error {
client := c.GetClient()
if client == nil {
return context.Canceled
}
return client.Ping(ctx).Err()
}
-125
View File
@@ -1,125 +0,0 @@
package reconnect
import "time"
// Strategy 重连策略接口
type Strategy interface {
// NextDelay 返回下一次重试的延迟时间
// attempt 是当前重试次数(从1开始)
NextDelay(attempt int) time.Duration
// Reset 重置策略状态
Reset()
}
// ExponentialBackoff 指数退避策略
type ExponentialBackoff struct {
initialDelay time.Duration
maxDelay time.Duration
multiplier float64
}
// NewExponentialBackoff 创建指数退避策略
func NewExponentialBackoff(initialDelay, maxDelay time.Duration, multiplier float64) *ExponentialBackoff {
if multiplier <= 0 {
multiplier = 2.0
}
return &ExponentialBackoff{
initialDelay: initialDelay,
maxDelay: maxDelay,
multiplier: multiplier,
}
}
// NextDelay 计算下一次延迟时间
func (e *ExponentialBackoff) NextDelay(attempt int) time.Duration {
if attempt <= 0 {
attempt = 1
}
delay := float64(e.initialDelay)
for i := 1; i < attempt; i++ {
delay *= e.multiplier
if time.Duration(delay) >= e.maxDelay {
return e.maxDelay
}
}
if time.Duration(delay) > e.maxDelay {
return e.maxDelay
}
return time.Duration(delay)
}
// Reset 重置策略
func (e *ExponentialBackoff) Reset() {
// 无状态,无需重置
}
// FixedInterval 固定间隔策略
type FixedInterval struct {
interval time.Duration
}
// NewFixedInterval 创建固定间隔策略
func NewFixedInterval(interval time.Duration) *FixedInterval {
return &FixedInterval{
interval: interval,
}
}
// NextDelay 返回固定延迟时间
func (f *FixedInterval) NextDelay(attempt int) time.Duration {
return f.interval
}
// Reset 重置策略
func (f *FixedInterval) Reset() {
// 无状态,无需重置
}
// LinearBackoff 线性退避策略
type LinearBackoff struct {
initialDelay time.Duration
maxDelay time.Duration
increment time.Duration
}
// NewLinearBackoff 创建线性退避策略
func NewLinearBackoff(initialDelay, maxDelay, increment time.Duration) *LinearBackoff {
return &LinearBackoff{
initialDelay: initialDelay,
maxDelay: maxDelay,
increment: increment,
}
}
// NextDelay 计算下一次延迟时间
func (l *LinearBackoff) NextDelay(attempt int) time.Duration {
if attempt <= 0 {
attempt = 1
}
delay := l.initialDelay + time.Duration(attempt-1)*l.increment
if delay > l.maxDelay {
return l.maxDelay
}
return delay
}
// Reset 重置策略
func (l *LinearBackoff) Reset() {
// 无状态,无需重置
}
// NewStrategy 根据配置创建策略
func NewStrategy(cfg Config) Strategy {
switch cfg.Strategy {
case StrategyFixedInterval:
return NewFixedInterval(cfg.InitialDelay)
case StrategyLinearBackoff:
return NewLinearBackoff(cfg.InitialDelay, cfg.MaxDelay, cfg.LinearIncrement)
case StrategyExponentialBackoff:
fallthrough
default:
return NewExponentialBackoff(cfg.InitialDelay, cfg.MaxDelay, cfg.Multiplier)
}
}