修复(#1): 服务器用户列表缓存按 protocol 隔离 + 兜底不写缓存
服务器用户列表缓存跨协议污染修复(HIF-1 / 详见 PR #5 四件套): 1. 缓存 key 加 protocol 维度(`server:user:{server_id}:{protocol}`),对齐 ServerConfig 已有约定 2. 显式枚举协议清除用户列表缓存(AllProtocols + ServerUserListCacheKeysForServer),不用 SCAN 3. 三个兜底分支不写缓存 + Errorw 日志(带 server_id + protocol 字段) 4. hysteria2 → hysteria 兼容归一化 + 6 个新单测 Closes HIF-1
This commit is contained in:
@@ -25,6 +25,31 @@ const (
|
||||
ServerConfigCacheKey = "server:config:"
|
||||
)
|
||||
|
||||
// AllProtocols 枚举所有客户端可能携带的 protocol。
|
||||
// 用户列表缓存 key 形态为 `server:user:{server_id}:{protocol}`,按 server_id
|
||||
// 失效时需要按协议精确删除。SCAN 在增量 rehash 期间可能漏 key,因此采用显式枚举。
|
||||
// 包含 hysteria2(兼容字段,与 hysteria 同语义),多余的 Del 是 no-op,over-deletion 安全。
|
||||
var AllProtocols = []string{
|
||||
"shadowsocks",
|
||||
"vmess",
|
||||
"vless",
|
||||
"trojan",
|
||||
"anytls",
|
||||
"tuic",
|
||||
"hysteria",
|
||||
"hysteria2",
|
||||
}
|
||||
|
||||
// ServerUserListCacheKeysForServer 返回给定 server 的所有 protocol 维度缓存 key。
|
||||
// 用于 Del 路径——节点 / 订阅 / 流量统计触发缓存失效时一次性清掉该 server 下所有协议条目。
|
||||
func ServerUserListCacheKeysForServer(serverId int64) []string {
|
||||
keys := make([]string, 0, len(AllProtocols))
|
||||
for _, protocol := range AllProtocols {
|
||||
keys = append(keys, fmt.Sprintf("%s%d:%s", ServerUserListCacheKey, serverId, protocol))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// FilterParams Filter Server Params
|
||||
type FilterParams struct {
|
||||
Page int
|
||||
@@ -134,7 +159,7 @@ func (m *customServerModel) ClearNodeCache(ctx context.Context, params *FilterNo
|
||||
}
|
||||
var cacheKeys []string
|
||||
for _, node := range nodes {
|
||||
cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, node.ServerId))
|
||||
cacheKeys = append(cacheKeys, ServerUserListCacheKeysForServer(node.ServerId)...)
|
||||
if node.Protocol != "" {
|
||||
var cursor uint64
|
||||
for {
|
||||
@@ -162,8 +187,7 @@ func (m *customServerModel) ClearNodeCache(ctx context.Context, params *FilterNo
|
||||
|
||||
// ClearServerCache Clear Server Cache
|
||||
func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64) error {
|
||||
var cacheKeys []string
|
||||
cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, serverId))
|
||||
cacheKeys := ServerUserListCacheKeysForServer(serverId)
|
||||
var cursor uint64
|
||||
for {
|
||||
keys, newCursor, err := m.Cache.Scan(ctx, cursor, fmt.Sprintf("%s%d*", ServerConfigCacheKey, serverId), 100).Result()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestServerUserListCacheKeysForServer 验证按 server 枚举出的协议缓存 key 覆盖
|
||||
// 所有客户端可能携带的 protocol(包括 hysteria2 兼容字段),形态为
|
||||
// `server:user:{server_id}:{protocol}`。HIF-1: Del 路径必须用枚举而非 SCAN。
|
||||
func TestServerUserListCacheKeysForServer(t *testing.T) {
|
||||
const serverId int64 = 42
|
||||
|
||||
keys := ServerUserListCacheKeysForServer(serverId)
|
||||
if len(keys) != len(AllProtocols) {
|
||||
t.Fatalf("expected %d keys, got %d: %v", len(AllProtocols), len(keys), keys)
|
||||
}
|
||||
|
||||
got := make([]string, len(keys))
|
||||
copy(got, keys)
|
||||
sort.Strings(got)
|
||||
|
||||
want := make([]string, 0, len(AllProtocols))
|
||||
for _, p := range AllProtocols {
|
||||
want = append(want, fmt.Sprintf("%s%d:%s", ServerUserListCacheKey, serverId, p))
|
||||
}
|
||||
sort.Strings(want)
|
||||
|
||||
for i := range got {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("key[%d] = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerUserListCacheKeysForServer_NoLegacyFlatKey 验证清缓存路径不再使用
|
||||
// 旧的扁平 key `server:user:{server_id}`(无 protocol 维度)——旧 key 一旦再出现,
|
||||
// 与 P0-1 写入的带 protocol key 形态不一致,Del 会失效,回归 HIF-1 缺陷。
|
||||
func TestServerUserListCacheKeysForServer_NoLegacyFlatKey(t *testing.T) {
|
||||
const serverId int64 = 7
|
||||
|
||||
legacy := fmt.Sprintf("%s%d", ServerUserListCacheKey, serverId)
|
||||
for _, key := range ServerUserListCacheKeysForServer(serverId) {
|
||||
if key == legacy {
|
||||
t.Fatalf("Del path still emits legacy flat key %q without protocol", legacy)
|
||||
}
|
||||
if !strings.HasPrefix(key, legacy+":") {
|
||||
t.Errorf("key %q does not match `<flat>:<protocol>` shape", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllProtocolsContainsKnownProtocols 兜底保证 AllProtocols 至少覆盖关键协议;
|
||||
// 缺失任一会让对应协议的用户列表缓存清不掉。
|
||||
func TestAllProtocolsContainsKnownProtocols(t *testing.T) {
|
||||
required := []string{
|
||||
"shadowsocks",
|
||||
"vmess",
|
||||
"vless",
|
||||
"trojan",
|
||||
"tuic",
|
||||
"hysteria",
|
||||
"hysteria2",
|
||||
}
|
||||
set := make(map[string]struct{}, len(AllProtocols))
|
||||
for _, p := range AllProtocols {
|
||||
set[p] = struct{}{}
|
||||
}
|
||||
for _, r := range required {
|
||||
if _, ok := set[r]; !ok {
|
||||
t.Errorf("AllProtocols missing %q", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user