Compare commits
13
Commits
main
..
b7e9560bdc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7e9560bdc | ||
|
|
2aedf6314f | ||
|
|
b90d7ec808 | ||
|
|
a47462ffd4 | ||
|
|
dfe2b6c806 | ||
|
|
c68b77b1b7 | ||
|
|
597e1cf840 | ||
|
|
4382d345f6 | ||
|
|
a8180f5115 | ||
|
|
915b5dd0bb | ||
|
|
896d518c54 | ||
|
|
c70c2e9f1c | ||
|
|
65fe52ceb0 |
@@ -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.
|
||||
@@ -1,25 +1,15 @@
|
||||
# Reconnect - Go 连接重连库
|
||||
# reconnect
|
||||
|
||||
[](https://golang.org)
|
||||
[](LICENSE)
|
||||
|
||||
一个用于 Go 语言的智能连接重连库,支持数据库、Redis、etcd、gRPC 等多种服务的自动重连和健康检查。
|
||||
一个通用的Go语言重连库,支持Redis、PostgreSQL和etcd的自动重连功能。
|
||||
|
||||
## 特性
|
||||
|
||||
- 🔄 **智能重连**: 支持多种重连策略(指数退避、固定间隔、线性退避)
|
||||
- 💗 **健康检查**: 定期检查连接状态,自动发现问题并重连
|
||||
- 🔌 **多服务支持**: 内置支持数据库(GORM)、Redis、etcd、gRPC
|
||||
- ⚙️ **高度可配置**: 灵活的配置选项,满足不同场景需求
|
||||
- 📊 **状态监控**: 实时连接状态监控和回调通知
|
||||
- 🛡️ **线程安全**: 完全的并发安全设计
|
||||
|
||||
## 支持的服务
|
||||
|
||||
- **数据库**: 通过 GORM 支持 MySQL、PostgreSQL、SQLite 等
|
||||
- **Redis**: 基于 go-redis/v8 客户端
|
||||
- **etcd**: 基于 etcd client/v3
|
||||
- **gRPC**: 基于 google.golang.org/grpc
|
||||
- 🔄 自动重连机制
|
||||
- ⚙️ 可配置的重连策略
|
||||
- 🔍 连接健康检查
|
||||
- 📊 连接状态监控
|
||||
- 🎯 支持多种服务(Redis、PostgreSQL、etcd)
|
||||
- 🛡️ 线程安全
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -29,101 +19,88 @@ go get git.whblueocean.cn/blueocean-go/reconnect
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 数据库重连
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"git.whblueocean.cn/blueocean-go/reconnect"
|
||||
"github.com/blueocean-go/reconnect"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 配置 Redis 连接
|
||||
config := reconnect.RedisClientConfig{
|
||||
Options: &redis.Options{
|
||||
options := &redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
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)
|
||||
},
|
||||
},
|
||||
Password: "",
|
||||
DB: 0,
|
||||
}
|
||||
|
||||
// 创建客户端
|
||||
client, err := reconnect.NewRedisClient(config)
|
||||
if err != nil {
|
||||
config := reconnect.DefaultConfig()
|
||||
config.RetryInterval = 3 * time.Second
|
||||
config.OnReconnect = func() {
|
||||
fmt.Println("Redis重连成功!")
|
||||
}
|
||||
|
||||
manager := reconnect.RedisManager(options, config)
|
||||
|
||||
ctx := context.Background()
|
||||
if err := manager.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer manager.Stop()
|
||||
|
||||
// 使用 Redis
|
||||
rdb := client.GetClient()
|
||||
// ... 执行 Redis 操作
|
||||
// 使用Redis客户端
|
||||
redisClient := reconnect.NewRedisClient(options)
|
||||
redisClient.Connect(ctx)
|
||||
client := redisClient.GetClient()
|
||||
// ... 使用client进行操作
|
||||
}
|
||||
```
|
||||
|
||||
### PostgreSQL重连
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/blueocean-go/reconnect"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dsn := "host=localhost user=postgres password=postgres dbname=testdb sslmode=disable"
|
||||
|
||||
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进行操作
|
||||
}
|
||||
```
|
||||
|
||||
@@ -133,315 +110,109 @@ func main() {
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.whblueocean.cn/blueocean-go/reconnect"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"github.com/blueocean-go/reconnect"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 配置 etcd 连接
|
||||
config := reconnect.EtcdClientConfig{
|
||||
etcdConfig := clientv3.Config{
|
||||
Endpoints: []string{"localhost:2379"},
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
// 创建客户端
|
||||
client, err := reconnect.NewEtcdClient(config)
|
||||
if err != nil {
|
||||
config := reconnect.DefaultConfig()
|
||||
config.RetryInterval = 3 * time.Second
|
||||
config.OnReconnect = func() {
|
||||
fmt.Println("etcd重连成功!")
|
||||
}
|
||||
|
||||
manager := reconnect.EtcdManager(etcdConfig, config)
|
||||
|
||||
ctx := context.Background()
|
||||
if err := manager.Start(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
defer manager.Stop()
|
||||
|
||||
// 使用 etcd
|
||||
etcdClient := client.GetClient()
|
||||
// ... 执行 etcd 操作
|
||||
}
|
||||
```
|
||||
|
||||
### 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 秒
|
||||
// 使用etcd客户端
|
||||
etcdClient := reconnect.NewEtcdClient(etcdConfig)
|
||||
etcdClient.Connect(ctx)
|
||||
client := etcdClient.GetClient()
|
||||
// ... 使用client进行操作
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `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
|
||||
type CustomConnector struct {
|
||||
// 自定义连接逻辑
|
||||
}
|
||||
type Config struct {
|
||||
// MaxRetries 最大重试次数,0表示无限重试
|
||||
MaxRetries int
|
||||
|
||||
func (c *CustomConnector) Connect(ctx context.Context) error {
|
||||
// 实现连接逻辑
|
||||
return nil
|
||||
}
|
||||
// RetryInterval 重试间隔
|
||||
RetryInterval time.Duration
|
||||
|
||||
func (c *CustomConnector) Close() error {
|
||||
// 实现关闭逻辑
|
||||
return nil
|
||||
}
|
||||
// Timeout 连接超时时间
|
||||
Timeout time.Duration
|
||||
|
||||
type CustomHealthChecker struct {
|
||||
// 自定义健康检查逻辑
|
||||
}
|
||||
// OnReconnect 重连成功后的回调函数
|
||||
OnReconnect func()
|
||||
|
||||
func (c *CustomHealthChecker) HealthCheck(ctx context.Context) error {
|
||||
// 实现健康检查逻辑
|
||||
return nil
|
||||
// OnDisconnect 断开连接时的回调函数
|
||||
OnDisconnect func(error)
|
||||
}
|
||||
|
||||
// 使用自定义连接器
|
||||
connector := &CustomConnector{}
|
||||
checker := &CustomHealthChecker{}
|
||||
manager := reconnect.NewConnectionManager(connector, checker, config)
|
||||
```
|
||||
|
||||
### 手动触发重连
|
||||
## API文档
|
||||
|
||||
```go
|
||||
// 手动触发重连
|
||||
client.TriggerReconnect()
|
||||
### Manager
|
||||
|
||||
// 获取当前状态
|
||||
state := client.State()
|
||||
fmt.Printf("当前状态: %s\n", state)
|
||||
```
|
||||
重连管理器,负责监控连接状态并自动重连。
|
||||
|
||||
## 最佳实践
|
||||
- `Start(ctx context.Context) error` - 启动连接并开始监控
|
||||
- `Stop() error` - 停止重连管理器
|
||||
- `IsConnected() bool` - 返回当前连接状态
|
||||
- `WaitForError() error` - 等待错误(用于阻塞等待)
|
||||
|
||||
1. **合理设置重试次数**: 对于关键服务建议设置为无限重试,非关键服务设置合理的重试上限
|
||||
2. **配置适当的健康检查间隔**: 太频繁会影响性能,太稀疏会延迟发现问题
|
||||
3. **使用回调函数监控状态**: 通过 `OnStateChange`、`OnReconnect`、`OnError` 回调监控连接状态
|
||||
4. **设置合适的超时时间**: 避免长时间阻塞
|
||||
5. **优雅关闭**: 在应用关闭时调用 `Close()` 方法释放资源
|
||||
### RedisClient
|
||||
|
||||
## 依赖
|
||||
Redis客户端包装器。
|
||||
|
||||
- [go-redis/v8](https://github.com/go-redis/redis) - Redis 客户端
|
||||
- [etcd client/v3](https://etcd.io/) - etcd 客户端
|
||||
- [google.golang.org/grpc](https://grpc.io/) - gRPC 框架
|
||||
- [gorm.io/gorm](https://gorm.io/) - ORM 框架
|
||||
- `Connect(ctx context.Context) error` - 建立连接
|
||||
- `Close() error` - 关闭连接
|
||||
- `Ping(ctx context.Context) error` - 健康检查
|
||||
- `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
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.1.0
|
||||
- 新增 RabbitMQ 重连支持
|
||||
- 添加 RabbitMQ 客户端封装和健康检查
|
||||
- 完善文档和使用示例
|
||||
|
||||
### v1.0.0
|
||||
- 初始版本发布
|
||||
- 支持数据库、Redis、etcd、gRPC 重连
|
||||
- 实现三种重连策略
|
||||
Copyright (c) 2025 blueocean-go
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -2,143 +2,107 @@ package reconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
// EtcdClientConfig etcd客户端配置
|
||||
type EtcdClientConfig struct {
|
||||
// Endpoints etcd服务地址列表
|
||||
Endpoints []string
|
||||
// DialTimeout 连接超时时间
|
||||
DialTimeout time.Duration
|
||||
// Username 用户名(可选)
|
||||
Username string
|
||||
// Password 密码(可选)
|
||||
Password string
|
||||
// ReconnectConfig 重连配置
|
||||
ReconnectConfig Config
|
||||
}
|
||||
|
||||
// EtcdClient 带重连功能的etcd客户端
|
||||
// EtcdClient etcd客户端包装器
|
||||
type EtcdClient struct {
|
||||
config EtcdClientConfig
|
||||
config clientv3.Config
|
||||
client *clientv3.Client
|
||||
manager *ConnectionManager
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// etcdConnector etcd连接器
|
||||
type etcdConnector struct {
|
||||
client *EtcdClient
|
||||
// NewEtcdClient 创建新的etcd客户端
|
||||
func NewEtcdClient(config clientv3.Config) *EtcdClient {
|
||||
return &EtcdClient{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *etcdConnector) Connect(ctx context.Context) error {
|
||||
e.client.mu.Lock()
|
||||
defer e.client.mu.Unlock()
|
||||
// Connect 建立etcd连接
|
||||
func (e *EtcdClient) Connect(ctx context.Context) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
cfg := clientv3.Config{
|
||||
Endpoints: e.client.config.Endpoints,
|
||||
DialTimeout: e.client.config.DialTimeout,
|
||||
Username: e.client.config.Username,
|
||||
Password: e.client.config.Password,
|
||||
if e.client != nil {
|
||||
_ = e.client.Close()
|
||||
}
|
||||
|
||||
if cfg.DialTimeout == 0 {
|
||||
cfg.DialTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
cli, err := clientv3.New(cfg)
|
||||
client, err := clientv3.New(e.config)
|
||||
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()
|
||||
|
||||
_, err = cli.Status(ctx, cfg.Endpoints[0])
|
||||
_, err = client.Status(timeoutCtx, e.config.Endpoints[0])
|
||||
if err != nil {
|
||||
cli.Close()
|
||||
return err
|
||||
_ = client.Close()
|
||||
return fmt.Errorf("etcd status check failed: %w", err)
|
||||
}
|
||||
|
||||
e.client.client = cli
|
||||
e.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *etcdConnector) Close() error {
|
||||
e.client.mu.Lock()
|
||||
defer e.client.mu.Unlock()
|
||||
// Close 关闭etcd连接
|
||||
func (e *EtcdClient) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.client.client != nil {
|
||||
err := e.client.client.Close()
|
||||
e.client.client = nil
|
||||
if e.client != nil {
|
||||
err := e.client.Close()
|
||||
e.client = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// etcdHealthChecker etcd健康检查器
|
||||
type etcdHealthChecker struct {
|
||||
client *EtcdClient
|
||||
// Ping 检查etcd连接是否健康
|
||||
func (e *EtcdClient) Ping(ctx context.Context) error {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
if e.client == nil {
|
||||
return fmt.Errorf("etcd client is nil")
|
||||
}
|
||||
|
||||
func (e *etcdHealthChecker) HealthCheck(ctx context.Context) error {
|
||||
e.client.mu.RLock()
|
||||
cli := e.client.client
|
||||
endpoints := e.client.config.Endpoints
|
||||
e.client.mu.RUnlock()
|
||||
|
||||
if cli == nil || len(endpoints) == 0 {
|
||||
return context.Canceled
|
||||
if len(e.config.Endpoints) == 0 {
|
||||
return fmt.Errorf("etcd endpoints is empty")
|
||||
}
|
||||
|
||||
_, err := cli.Status(ctx, endpoints[0])
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := e.client.Status(timeoutCtx, e.config.Endpoints[0])
|
||||
return err
|
||||
}
|
||||
|
||||
// NewEtcdClient 创建带重连功能的etcd客户端
|
||||
func NewEtcdClient(cfg EtcdClientConfig) (*EtcdClient, error) {
|
||||
client := &EtcdClient{
|
||||
config: cfg,
|
||||
// IsConnected 检查连接状态
|
||||
func (e *EtcdClient) IsConnected() bool {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
return e.client != nil
|
||||
}
|
||||
|
||||
connector := &etcdConnector{client: client}
|
||||
checker := &etcdHealthChecker{client: client}
|
||||
// GetClient 获取底层的etcd客户端
|
||||
func (e *EtcdClient) GetClient() *clientv3.Client {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
client.manager = NewConnectionManager(connector, checker, cfg.ReconnectConfig)
|
||||
|
||||
// 首次连接
|
||||
ctx := context.Background()
|
||||
if err := client.manager.ConnectWithRetry(ctx); err != nil {
|
||||
return nil, err
|
||||
return e.client
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetClient 获取etcd客户端
|
||||
func (c *EtcdClient) GetClient() *clientv3.Client {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.client
|
||||
}
|
||||
|
||||
// State 获取连接状态
|
||||
func (c *EtcdClient) State() ConnectionState {
|
||||
return c.manager.State()
|
||||
}
|
||||
|
||||
// Close 关闭客户端
|
||||
func (c *EtcdClient) Close() error {
|
||||
return c.manager.Close()
|
||||
}
|
||||
|
||||
// TriggerReconnect 手动触发重连
|
||||
func (c *EtcdClient) TriggerReconnect() {
|
||||
c.manager.TriggerReconnect()
|
||||
// EtcdManager 创建etcd重连管理器
|
||||
func EtcdManager(config clientv3.Config, reconnectConfig *Config) *Manager {
|
||||
client := NewEtcdClient(config)
|
||||
return NewManager(client, reconnectConfig)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -1,33 +1,31 @@
|
||||
module git.whblueocean.cn/blueocean-go/reconnect
|
||||
|
||||
go 1.24.0
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
go.etcd.io/etcd/client/v3 v3.6.5
|
||||
google.golang.org/grpc v1.75.1
|
||||
gorm.io/gorm v1.31.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/redis/go-redis/v9 v9.3.0
|
||||
go.etcd.io/etcd/client/v3 v3.5.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.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/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.5 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.10 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.17.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,76 +1,56 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
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/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/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
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.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
|
||||
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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.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.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk=
|
||||
go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U=
|
||||
go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
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=
|
||||
go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k=
|
||||
go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
|
||||
go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao=
|
||||
go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
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-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-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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
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-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/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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-20191204190536-9bdfabe68543/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=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
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-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.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/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=
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -2,125 +2,87 @@ package reconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisClientConfig Redis客户端配置
|
||||
type RedisClientConfig struct {
|
||||
// Options Redis连接选项
|
||||
Options *redis.Options
|
||||
// ReconnectConfig 重连配置
|
||||
ReconnectConfig Config
|
||||
}
|
||||
|
||||
// RedisClient 带重连功能的Redis客户端
|
||||
// RedisClient Redis客户端包装器
|
||||
type RedisClient struct {
|
||||
config RedisClientConfig
|
||||
options *redis.Options
|
||||
client *redis.Client
|
||||
manager *ConnectionManager
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// redisConnector Redis连接器
|
||||
type redisConnector struct {
|
||||
client *RedisClient
|
||||
// NewRedisClient 创建新的Redis客户端
|
||||
func NewRedisClient(options *redis.Options) *RedisClient {
|
||||
return &RedisClient{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *redisConnector) Connect(ctx context.Context) error {
|
||||
r.client.mu.Lock()
|
||||
defer r.client.mu.Unlock()
|
||||
// Connect 建立Redis连接
|
||||
func (r *RedisClient) Connect(ctx context.Context) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
rdb := redis.NewClient(r.client.config.Options)
|
||||
|
||||
// 验证连接
|
||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||
rdb.Close()
|
||||
return err
|
||||
if r.client != nil {
|
||||
_ = r.client.Close()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (r *redisConnector) Close() error {
|
||||
r.client.mu.Lock()
|
||||
defer r.client.mu.Unlock()
|
||||
// Close 关闭Redis连接
|
||||
func (r *RedisClient) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.client.client != nil {
|
||||
err := r.client.client.Close()
|
||||
r.client.client = nil
|
||||
if r.client != nil {
|
||||
err := r.client.Close()
|
||||
r.client = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// redisHealthChecker Redis健康检查器
|
||||
type redisHealthChecker struct {
|
||||
client *RedisClient
|
||||
// Ping 检查Redis连接是否健康
|
||||
func (r *RedisClient) Ping(ctx context.Context) error {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if r.client == nil {
|
||||
return fmt.Errorf("redis client is nil")
|
||||
}
|
||||
|
||||
func (r *redisHealthChecker) HealthCheck(ctx context.Context) error {
|
||||
r.client.mu.RLock()
|
||||
rdb := r.client.client
|
||||
r.client.mu.RUnlock()
|
||||
|
||||
if rdb == nil {
|
||||
return context.Canceled
|
||||
return r.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
return rdb.Ping(ctx).Err()
|
||||
// IsConnected 检查连接状态
|
||||
func (r *RedisClient) IsConnected() bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
return r.client != nil
|
||||
}
|
||||
|
||||
// NewRedisClient 创建带重连功能的Redis客户端
|
||||
func NewRedisClient(cfg RedisClientConfig) (*RedisClient, error) {
|
||||
client := &RedisClient{
|
||||
config: cfg,
|
||||
// GetClient 获取底层的Redis客户端
|
||||
func (r *RedisClient) GetClient() *redis.Client {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
return r.client
|
||||
}
|
||||
|
||||
connector := &redisConnector{client: client}
|
||||
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
|
||||
// RedisManager 创建Redis重连管理器
|
||||
func RedisManager(options *redis.Options, config *Config) *Manager {
|
||||
client := NewRedisClient(options)
|
||||
return NewManager(client, config)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetClient 获取Redis客户端
|
||||
func (c *RedisClient) GetClient() *redis.Client {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.client
|
||||
}
|
||||
|
||||
// State 获取连接状态
|
||||
func (c *RedisClient) State() ConnectionState {
|
||||
return c.manager.State()
|
||||
}
|
||||
|
||||
// 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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user