mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
fix(redis): add maintenance notification startup switch
Default Redis maintenance notification negotiation to disabled and apply the startup-only setting to both platform and Asynq clients.
This commit is contained in:
@@ -49,6 +49,8 @@ REDIS_ADDR=redis:6379
|
||||
# REDIS_DB=0
|
||||
REDIS_KEY_PREFIX=openflare:
|
||||
# REDIS_POOL_SIZE=100
|
||||
# 启动时开关;修改后需重启服务
|
||||
REDIS_MAINT_NOTIFICATIONS=false
|
||||
# compose 宿主机映射端口(仅 docker-compose 使用)
|
||||
# REDIS_PORT=6379
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ redis:
|
||||
max_retries: 3
|
||||
pool_timeout: 4
|
||||
conn_max_idle_time: 300
|
||||
maint_notifications: false # 启动时开关;启用 Redis maintenance notifications 自动协商,修改后需重启
|
||||
|
||||
# ─── Logging ────────────────────────────────────────────────────────────────────
|
||||
log:
|
||||
|
||||
@@ -36,6 +36,7 @@ sidebar: false
|
||||
### 修复
|
||||
|
||||
- GORM SQL 语句改为仅在全局 `debug` 日志级别输出;生产默认日志级别不再记录查询文本,慢查询与执行错误也不包含 SQL 参数,避免节点访问令牌等敏感信息写入应用日志。
|
||||
- 新增 `redis.maint_notifications` 启动开关并默认关闭,平台与 Asynq Redis 客户端仅在显式开启时进行 maintenance notifications 自动协商,避免不支持该命令的 Redis 服务持续输出握手 fallback 警告。
|
||||
- WAF 规则编辑器支持通过按钮或键盘删除普通节点和连线,节点属性栏改为选中节点后按需显示,并修复拖动节点时因 React Flow 初始化状态丢失导致的画面闪烁与 error 015。
|
||||
- WAF 规则编辑器新增启用/停用控制,并移除已废弃的规则组侧站点绑定入口;站点规则顺序统一在反代路由详情中管理。
|
||||
- 修复 Agent 心跳无法从活动 WAF 运行图发现 IP 组引用的问题,并对发布与同步的完整 IP 组快照增加 20 MiB 聚合容量保护。
|
||||
|
||||
@@ -97,6 +97,7 @@ Server 的所有核心基础配置定义在 `config.yaml` 中,且均支持环
|
||||
| `redis.db` | `REDIS_DB` | Redis 逻辑数据库编号 | `0` |
|
||||
| `redis.key_prefix` | `REDIS_KEY_PREFIX` | 系统在 Redis 中使用的键前缀 | `openflare:` |
|
||||
| `redis.pool_size` | `REDIS_POOL_SIZE` | Redis 连接池大小 | `100` |
|
||||
| `redis.maint_notifications` | `REDIS_MAINT_NOTIFICATIONS` | 启动时是否启用 Redis maintenance notifications 自动协商;兼容性不明时保持关闭,修改后需重启 | `false` |
|
||||
|
||||
### 4. ClickHouse 配置 (`clickhouse:`)
|
||||
|
||||
|
||||
@@ -253,6 +253,7 @@ func applyEnvOverrides(c *configModel) {
|
||||
c.Redis.DB = envInt("REDIS_DB", c.Redis.DB)
|
||||
c.Redis.KeyPrefix = envStr("REDIS_KEY_PREFIX", c.Redis.KeyPrefix)
|
||||
c.Redis.PoolSize = envInt("REDIS_POOL_SIZE", c.Redis.PoolSize)
|
||||
c.Redis.MaintNotifications = envBool("REDIS_MAINT_NOTIFICATIONS", c.Redis.MaintNotifications)
|
||||
|
||||
// ─── ClickHouse ───
|
||||
if v, ok := os.LookupEnv("CLICKHOUSE_HOST"); ok {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestApplyEnvOverridesRedisMaintNotifications(t *testing.T) {
|
||||
t.Setenv("REDIS_MAINT_NOTIFICATIONS", "true")
|
||||
|
||||
cfg := &configModel{}
|
||||
applyEnvOverrides(cfg)
|
||||
|
||||
if !cfg.Redis.MaintNotifications {
|
||||
t.Fatal("REDIS_MAINT_NOTIFICATIONS=true was not applied")
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -88,22 +88,23 @@ type clickHouseConfig struct {
|
||||
|
||||
// redisConfig Redis配置
|
||||
type redisConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Addrs []string `mapstructure:"addrs"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
ClusterMode bool `mapstructure:"cluster_mode"`
|
||||
MasterName string `mapstructure:"master_name"`
|
||||
KeyPrefix string `mapstructure:"key_prefix"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
MinIdleConn int `mapstructure:"min_idle_conn"`
|
||||
DialTimeout int `mapstructure:"dial_timeout"`
|
||||
ReadTimeout int `mapstructure:"read_timeout"`
|
||||
WriteTimeout int `mapstructure:"write_timeout"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
PoolTimeout int `mapstructure:"pool_timeout"`
|
||||
ConnMaxIdleTime int `mapstructure:"conn_max_idle_time"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Addrs []string `mapstructure:"addrs"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
ClusterMode bool `mapstructure:"cluster_mode"`
|
||||
MasterName string `mapstructure:"master_name"`
|
||||
KeyPrefix string `mapstructure:"key_prefix"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
MinIdleConn int `mapstructure:"min_idle_conn"`
|
||||
DialTimeout int `mapstructure:"dial_timeout"`
|
||||
ReadTimeout int `mapstructure:"read_timeout"`
|
||||
WriteTimeout int `mapstructure:"write_timeout"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
PoolTimeout int `mapstructure:"pool_timeout"`
|
||||
ConnMaxIdleTime int `mapstructure:"conn_max_idle_time"`
|
||||
MaintNotifications bool `mapstructure:"maint_notifications"`
|
||||
}
|
||||
|
||||
// logConfig 日志配置
|
||||
|
||||
+41
-32
@@ -35,45 +35,46 @@ func init() {
|
||||
if cfg.ClusterMode {
|
||||
// Cluster 模式
|
||||
Redis = redis.NewClusterClient(&redis.ClusterOptions{
|
||||
Addrs: cfg.Addrs,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
PoolSize: cfg.PoolSize,
|
||||
MinIdleConns: cfg.MinIdleConn,
|
||||
DialTimeout: time.Duration(cfg.DialTimeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.ReadTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.WriteTimeout) * time.Second,
|
||||
MaxRetries: cfg.MaxRetries,
|
||||
PoolTimeout: time.Duration(cfg.PoolTimeout) * time.Second,
|
||||
ConnMaxIdleTime: time.Duration(cfg.ConnMaxIdleTime) * time.Second,
|
||||
MaintNotificationsConfig: &maintnotifications.Config{
|
||||
Mode: maintnotifications.ModeDisabled,
|
||||
},
|
||||
Addrs: cfg.Addrs,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
PoolSize: cfg.PoolSize,
|
||||
MinIdleConns: cfg.MinIdleConn,
|
||||
DialTimeout: time.Duration(cfg.DialTimeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.ReadTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.WriteTimeout) * time.Second,
|
||||
MaxRetries: cfg.MaxRetries,
|
||||
PoolTimeout: time.Duration(cfg.PoolTimeout) * time.Second,
|
||||
ConnMaxIdleTime: time.Duration(cfg.ConnMaxIdleTime) * time.Second,
|
||||
MaintNotificationsConfig: redisMaintNotificationsConfig(cfg.MaintNotifications),
|
||||
})
|
||||
log.Println("[Redis] initialized in Cluster mode")
|
||||
} else {
|
||||
// Standalone 或 Sentinel 模式
|
||||
Redis = redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: cfg.Addrs,
|
||||
MasterName: cfg.MasterName, // 非空时启用 Sentinel
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
PoolSize: cfg.PoolSize,
|
||||
MinIdleConns: cfg.MinIdleConn,
|
||||
DialTimeout: time.Duration(cfg.DialTimeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.ReadTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.WriteTimeout) * time.Second,
|
||||
MaxRetries: cfg.MaxRetries,
|
||||
PoolTimeout: time.Duration(cfg.PoolTimeout) * time.Second,
|
||||
ConnMaxIdleTime: time.Duration(cfg.ConnMaxIdleTime) * time.Second,
|
||||
MaintNotificationsConfig: &maintnotifications.Config{
|
||||
Mode: maintnotifications.ModeDisabled,
|
||||
},
|
||||
})
|
||||
options := &redis.UniversalOptions{
|
||||
Addrs: cfg.Addrs,
|
||||
MasterName: cfg.MasterName, // 非空时启用 Sentinel
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
PoolSize: cfg.PoolSize,
|
||||
MinIdleConns: cfg.MinIdleConn,
|
||||
DialTimeout: time.Duration(cfg.DialTimeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.ReadTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.WriteTimeout) * time.Second,
|
||||
MaxRetries: cfg.MaxRetries,
|
||||
PoolTimeout: time.Duration(cfg.PoolTimeout) * time.Second,
|
||||
ConnMaxIdleTime: time.Duration(cfg.ConnMaxIdleTime) * time.Second,
|
||||
MaintNotificationsConfig: redisMaintNotificationsConfig(cfg.MaintNotifications),
|
||||
}
|
||||
if cfg.MasterName != "" {
|
||||
client := redis.NewFailoverClient(options.Failover())
|
||||
// FailoverOptions 暂不暴露该配置,在首次建连前写入客户端选项。
|
||||
client.Options().MaintNotificationsConfig = redisMaintNotificationsConfig(cfg.MaintNotifications)
|
||||
Redis = client
|
||||
log.Println("[Redis] initialized in Sentinel mode")
|
||||
} else {
|
||||
Redis = redis.NewUniversalClient(options)
|
||||
log.Println("[Redis] initialized in Standalone mode")
|
||||
}
|
||||
}
|
||||
@@ -97,6 +98,14 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func redisMaintNotificationsConfig(enabled bool) *maintnotifications.Config {
|
||||
mode := maintnotifications.ModeDisabled
|
||||
if enabled {
|
||||
mode = maintnotifications.ModeAuto
|
||||
}
|
||||
return &maintnotifications.Config{Mode: mode}
|
||||
}
|
||||
|
||||
// PrefixedKey 返回带前缀的 Key
|
||||
func PrefixedKey(key string) string {
|
||||
prefix := config.Config.Redis.KeyPrefix
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
)
|
||||
|
||||
func TestRedisMaintNotificationsConfig(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
enabled bool
|
||||
want maintnotifications.Mode
|
||||
}{
|
||||
{name: "disabled by default", enabled: false, want: maintnotifications.ModeDisabled},
|
||||
{name: "auto when enabled", enabled: true, want: maintnotifications.ModeAuto},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cfg := redisMaintNotificationsConfig(test.enabled)
|
||||
if cfg.Mode != test.want {
|
||||
t.Fatalf("maintenance notifications mode = %v, want %v", cfg.Mode, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+64
-16
@@ -7,8 +7,47 @@ package task
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
)
|
||||
|
||||
type redisClientConnOpt struct {
|
||||
options redis.Options
|
||||
}
|
||||
|
||||
func (opt redisClientConnOpt) MakeRedisClient() interface{} {
|
||||
return redis.NewClient(&opt.options)
|
||||
}
|
||||
|
||||
type redisClusterConnOpt struct {
|
||||
options redis.ClusterOptions
|
||||
}
|
||||
|
||||
func (opt redisClusterConnOpt) MakeRedisClient() interface{} {
|
||||
return redis.NewClusterClient(&opt.options)
|
||||
}
|
||||
|
||||
type redisFailoverConnOpt struct {
|
||||
options redis.FailoverOptions
|
||||
maintNotificationsEnabled bool
|
||||
}
|
||||
|
||||
func (opt redisFailoverConnOpt) MakeRedisClient() interface{} {
|
||||
client := redis.NewFailoverClient(&opt.options)
|
||||
// go-redis v9.16 does not expose maintenance notification settings on
|
||||
// FailoverOptions, so apply the configured mode before the client is used.
|
||||
client.Options().MaintNotificationsConfig = maintNotificationsConfig(opt.maintNotificationsEnabled)
|
||||
return client
|
||||
}
|
||||
|
||||
func maintNotificationsConfig(enabled bool) *maintnotifications.Config {
|
||||
mode := maintnotifications.ModeDisabled
|
||||
if enabled {
|
||||
mode = maintnotifications.ModeAuto
|
||||
}
|
||||
return &maintnotifications.Config{Mode: mode}
|
||||
}
|
||||
|
||||
// RedisOpt asynq Redis 连接配置(兼容 Standalone/Sentinel/Cluster)
|
||||
var RedisOpt asynq.RedisConnOpt
|
||||
|
||||
@@ -26,20 +65,26 @@ func NewRedisConnOpt() asynq.RedisConnOpt {
|
||||
addrs := cfg.Addrs
|
||||
|
||||
if cfg.ClusterMode {
|
||||
return asynq.RedisClusterClientOpt{
|
||||
Addrs: addrs,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
return redisClusterConnOpt{
|
||||
options: redis.ClusterOptions{
|
||||
Addrs: addrs,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
MaintNotificationsConfig: maintNotificationsConfig(cfg.MaintNotifications),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.MasterName != "" {
|
||||
return asynq.RedisFailoverClientOpt{
|
||||
MasterName: cfg.MasterName,
|
||||
SentinelAddrs: addrs,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
return redisFailoverConnOpt{
|
||||
maintNotificationsEnabled: cfg.MaintNotifications,
|
||||
options: redis.FailoverOptions{
|
||||
MasterName: cfg.MasterName,
|
||||
SentinelAddrs: addrs,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +92,15 @@ func NewRedisConnOpt() asynq.RedisConnOpt {
|
||||
if len(addrs) > 0 {
|
||||
addr = addrs[0]
|
||||
}
|
||||
return asynq.RedisClientOpt{
|
||||
Addr: addr,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
PoolSize: cfg.PoolSize,
|
||||
return redisClientConnOpt{
|
||||
options: redis.Options{
|
||||
Addr: addr,
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
PoolSize: cfg.PoolSize,
|
||||
MaintNotificationsConfig: maintNotificationsConfig(cfg.MaintNotifications),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
)
|
||||
|
||||
func TestNewRedisConnOptConfiguresMaintenanceNotifications(t *testing.T) {
|
||||
previous := config.Config.Redis
|
||||
t.Cleanup(func() { config.Config.Redis = previous })
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
enabled bool
|
||||
want maintnotifications.Mode
|
||||
}{
|
||||
{name: "disabled by default", enabled: false, want: maintnotifications.ModeDisabled},
|
||||
{name: "auto when enabled", enabled: true, want: maintnotifications.ModeAuto},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config.Config.Redis.MaintNotifications = test.enabled
|
||||
|
||||
t.Run("standalone", func(t *testing.T) {
|
||||
config.Config.Redis.ClusterMode = false
|
||||
config.Config.Redis.MasterName = ""
|
||||
config.Config.Redis.Addrs = []string{"127.0.0.1:6379"}
|
||||
|
||||
client, ok := NewRedisConnOpt().MakeRedisClient().(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("standalone option did not create *redis.Client")
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
assertMaintenanceNotificationsMode(t, client.Options().MaintNotificationsConfig, test.want)
|
||||
})
|
||||
|
||||
t.Run("cluster", func(t *testing.T) {
|
||||
config.Config.Redis.ClusterMode = true
|
||||
config.Config.Redis.MasterName = ""
|
||||
config.Config.Redis.Addrs = []string{"127.0.0.1:6379"}
|
||||
|
||||
client, ok := NewRedisConnOpt().MakeRedisClient().(*redis.ClusterClient)
|
||||
if !ok {
|
||||
t.Fatal("cluster option did not create *redis.ClusterClient")
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
assertMaintenanceNotificationsMode(t, client.Options().MaintNotificationsConfig, test.want)
|
||||
})
|
||||
|
||||
t.Run("sentinel", func(t *testing.T) {
|
||||
config.Config.Redis.ClusterMode = false
|
||||
config.Config.Redis.MasterName = "openflare"
|
||||
config.Config.Redis.Addrs = []string{"127.0.0.1:26379"}
|
||||
|
||||
client, ok := NewRedisConnOpt().MakeRedisClient().(*redis.Client)
|
||||
if !ok {
|
||||
t.Fatal("sentinel option did not create *redis.Client")
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
assertMaintenanceNotificationsMode(t, client.Options().MaintNotificationsConfig, test.want)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertMaintenanceNotificationsMode(t *testing.T, cfg *maintnotifications.Config, want maintnotifications.Mode) {
|
||||
t.Helper()
|
||||
if cfg == nil || cfg.Mode != want {
|
||||
t.Fatalf("maintenance notifications mode = %v, want %v", cfg, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user