diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 6f690eea..2c4d7dc4 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -23,6 +23,10 @@ sidebar: false ### 修复 +- 修复由于重构移除系统配置 Redis L2 缓存层后,遗留的系统配置缓存测试用例仍检查 Redis 物理键值导致测试失败的问题:改写测试为验证 L1 RAM 缓存行为,并在失效操作(Invalidate)后引入适当延迟以消除本地 Redis 广播异步被消费带来的测试竞态问题。 +- 修复数据库历史迁移代码在重构中丢失了 `tableExistsSQL` 和 `tablesWithPrefixSQL` 辅助函数定义,导致 `internal/db/migrator` 包和 `internal/cmd` 包编译失败的问题:在 `migrator.go` 底部重新实现并补齐了这俩函数的跨数据库方言支持。 +- 修复 Agent 包 IP 探测测试中,由于包级别缓存变量 `cachedIP` 跨用例污染导致 `TestLoadFallsBackToLocalIPWhenOutboundLookupFails` 最终获取到上个测试的 cached IP 从而报错失败的问题:在 `nodeip` 包中增加并导出 `ResetCacheForTest` 函数以在测试 Setup/Teardown 中清除缓存状态。 + - 修复旧版本迁移升级后,发布版本报错“版本号生成冲突,请重试”的问题。根本原因是配置版本表 `of_config_versions` 的自增主键 `id` 序列与导入的旧数据冲突;现重构配置版本表,将自增 `id` 移除,改由版本号字符串(如 `20260626-003`)直接作为主键(通过迁移 `202606270001_make_version_primary_key` 完成),并同步修改 Agent 和 Flared 模块中的排序及查询条件,解决删除 `id` 列后心跳上报报 `column "id" does not exist` 的故障。 - 修复代理路由详情页点击“发布配置”时,同时弹出配置差异对话框和确认发布对话框导致重叠的问题:点击发布时不再展示配置差异,直接进行确认发布。 - 修复配置版本发布到 Agent 后 `openresty -t` 因 `proxy_cache_path` 使用 `/var/cache/openresty` 导致非 root 用户 `mkdir` 失败的问题:发布快照与渲染将 `/var/` 下路径规范为 `__OPENFLARE_PROXY_CACHE_PATH__`,Agent 应用时落地为 `data_dir/var/cache/openflare_proxy` 并兼容重写已发布配置中的旧路径。 diff --git a/internal/apps/agent/config/config_test.go b/internal/apps/agent/config/config_test.go index 11649a1a..d27013b4 100644 --- a/internal/apps/agent/config/config_test.go +++ b/internal/apps/agent/config/config_test.go @@ -250,12 +250,14 @@ func TestLoadUsesEnvConfigWhenFileIsMissing(t *testing.T) { } func TestLoadDetectsOutboundIPWhenNodeIPMissing(t *testing.T) { + nodeip.ResetCacheForTest() previousLookup := nodeip.LookupOutboundIP nodeip.LookupOutboundIP = func(ctx context.Context, strategies ...geoip.OutboundIPStrategy) (net.IP, error) { return net.ParseIP("8.8.8.8"), nil } defer func() { nodeip.LookupOutboundIP = previousLookup + nodeip.ResetCacheForTest() }() dir := t.TempDir() @@ -283,6 +285,7 @@ func TestLoadDetectsOutboundIPWhenNodeIPMissing(t *testing.T) { } func TestLoadFallsBackToLocalIPWhenOutboundLookupFails(t *testing.T) { + nodeip.ResetCacheForTest() previousOutboundLookup := nodeip.LookupOutboundIP previousLocalLookup := nodeip.LookupLocalIP nodeip.LookupOutboundIP = func(ctx context.Context, strategies ...geoip.OutboundIPStrategy) (net.IP, error) { @@ -294,6 +297,7 @@ func TestLoadFallsBackToLocalIPWhenOutboundLookupFails(t *testing.T) { defer func() { nodeip.LookupOutboundIP = previousOutboundLookup nodeip.LookupLocalIP = previousLocalLookup + nodeip.ResetCacheForTest() }() dir := t.TempDir() diff --git a/internal/apps/config/public_config_cache_test.go b/internal/apps/config/public_config_cache_test.go index 635af846..369cfa76 100644 --- a/internal/apps/config/public_config_cache_test.go +++ b/internal/apps/config/public_config_cache_test.go @@ -7,7 +7,6 @@ import ( "context" "testing" - "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" "github.com/Rain-kl/Wavelet/internal/repository" "github.com/Rain-kl/Wavelet/internal/testhelper" @@ -46,13 +45,8 @@ func TestListVisibleSystemConfigsUsesRedisCache(t *testing.T) { } } - exists, err := db.Redis.Exists(ctx, db.PrefixedKey(repository.SystemConfigVisibleListRedisKey)).Result() - if err != nil { - t.Fatalf("Redis.Exists() error = %v", err) - } - if exists == 0 { - t.Fatal("expected visible config list cache key to exist") - } + // Since system configs are now purely cached in process-local RAM (L1) and not written to Redis (L2), + // we do not verify the existence of the legacy Redis key here. if err := repository.InvalidateVisibleSystemConfigsCache(ctx); err != nil { t.Fatalf("InvalidateVisibleSystemConfigsCache() error = %v", err) diff --git a/internal/apps/config/system_config_cache_test.go b/internal/apps/config/system_config_cache_test.go index 5841311c..a538b2f1 100644 --- a/internal/apps/config/system_config_cache_test.go +++ b/internal/apps/config/system_config_cache_test.go @@ -7,6 +7,7 @@ import ( "context" "errors" "testing" + "time" "github.com/redis/go-redis/v9" @@ -25,6 +26,7 @@ func TestSystemConfigRAMCacheServesUntilInvalidated(t *testing.T) { if err := repository.InvalidateAllSystemConfigCaches(ctx); err != nil { t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err) } + time.Sleep(50 * time.Millisecond) // Wait for async Redis broadcast to be processed warm, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySiteName) if err != nil { @@ -54,6 +56,7 @@ func TestSystemConfigRAMCacheServesUntilInvalidated(t *testing.T) { if err := repository.InvalidateSystemConfigCache(ctx, model.ConfigKeySiteName); err != nil { t.Fatalf("InvalidateSystemConfigCache(site_name) error = %v", err) } + time.Sleep(50 * time.Millisecond) // Wait for async Redis broadcast to be processed refreshed, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySiteName) if err != nil { @@ -63,13 +66,8 @@ func TestSystemConfigRAMCacheServesUntilInvalidated(t *testing.T) { t.Fatalf("GetSystemConfigByKey(site_name).Value = %q, want %q", refreshed.Value, "ram_probe_value") } - exists, err := db.Redis.HExists(ctx, db.PrefixedKey(repository.SystemConfigRedisHashKey), model.ConfigKeySiteName).Result() - if err != nil { - t.Fatalf("HExists(site_name) error = %v", err) - } - if !exists { - t.Fatal("expected redis hash field to be repopulated after refresh") - } + // Since system configs are now purely cached in process-local RAM (L1) and not written to Redis (L2), + // we do not verify if the Redis hash field is repopulated. } func TestInvalidateSystemConfigCacheClearsRedisField(t *testing.T) { @@ -86,6 +84,7 @@ func TestInvalidateSystemConfigCacheClearsRedisField(t *testing.T) { if err := repository.InvalidateSystemConfigCache(ctx, model.ConfigKeySiteName); err != nil { t.Fatalf("InvalidateSystemConfigCache(site_name) error = %v", err) } + time.Sleep(50 * time.Millisecond) // Wait for async Redis broadcast to be processed _, err = db.Redis.HGet(ctx, db.PrefixedKey(repository.SystemConfigRedisHashKey), model.ConfigKeySiteName).Result() if !errors.Is(err, redis.Nil) { diff --git a/internal/apps/edge/nodeip/nodeip.go b/internal/apps/edge/nodeip/nodeip.go index 6f6ac0ab..61233a93 100644 --- a/internal/apps/edge/nodeip/nodeip.go +++ b/internal/apps/edge/nodeip/nodeip.go @@ -104,3 +104,11 @@ func DetectLocal() string { } return bestIP } + +// ResetCacheForTest clears the cached IP. +func ResetCacheForTest() { + cacheMu.Lock() + cachedIP = "" + lastDetected = time.Time{} + cacheMu.Unlock() +} diff --git a/internal/db/migrator/migrator.go b/internal/db/migrator/migrator.go index 6929bbfe..1a56b21e 100644 --- a/internal/db/migrator/migrator.go +++ b/internal/db/migrator/migrator.go @@ -115,3 +115,17 @@ func clearSystemConfigCache() { log.Printf("[%s] clear system config cache failed: %v\n", dbType(), err) } } + +func tableExistsSQL(dialect string) string { + if dialect == dialectPostgres { + return "SELECT count(*) FROM information_schema.tables WHERE table_schema='public' AND table_name=$1" + } + return "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?" +} + +func tablesWithPrefixSQL(dialect string) string { + if dialect == dialectPostgres { + return "SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_name LIKE $1" + } + return "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ?" +} diff --git a/internal/repository/system_config_test.go b/internal/repository/system_config_test.go index 8dc601a3..0418229c 100644 --- a/internal/repository/system_config_test.go +++ b/internal/repository/system_config_test.go @@ -5,10 +5,13 @@ package repository import ( "context" + "encoding/json" "testing" + "time" "github.com/Rain-kl/Wavelet/internal/db" "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/pkg/cache/ram" "github.com/alicebob/miniredis/v2" "github.com/glebarez/sqlite" "github.com/redis/go-redis/v9" @@ -76,7 +79,7 @@ func TestListSystemConfigsByKeys_EmptyKeys(t *testing.T) { } } -func TestListSystemConfigsByKeys_LoadsFromRedisBeforeDB(t *testing.T) { +func TestListSystemConfigsByKeys_LoadsFromRAMBeforeDB(t *testing.T) { dbConn, cleanup := setupSystemConfigTest(t) defer cleanup() ctx := context.Background() @@ -85,6 +88,7 @@ func TestListSystemConfigsByKeys_LoadsFromRedisBeforeDB(t *testing.T) { if err := InvalidateAllSystemConfigCaches(ctx); err != nil { t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err) } + time.Sleep(50 * time.Millisecond) // Wait for async Redis broadcast to be processed warm, err := GetSystemConfigByKey(ctx, model.ConfigKeySiteName) if err != nil { @@ -100,8 +104,6 @@ func TestListSystemConfigsByKeys_LoadsFromRedisBeforeDB(t *testing.T) { t.Fatalf("Update(site_name) error = %v", err) } - ResetSystemConfigRAMCacheForTest() - configs, err := ListSystemConfigsByKeys(ctx, []string{model.ConfigKeySiteName}) if err != nil { t.Fatalf("ListSystemConfigsByKeys(site_name) error = %v", err) @@ -112,11 +114,28 @@ func TestListSystemConfigsByKeys_LoadsFromRedisBeforeDB(t *testing.T) { t.Fatal("ListSystemConfigsByKeys(site_name) missing site_name entry") } if sc.Value != "Wavelet" { - t.Fatalf("ListSystemConfigsByKeys(site_name).Value = %q, want redis value %q", sc.Value, "Wavelet") + t.Fatalf("ListSystemConfigsByKeys(site_name).Value = %q, want cached RAM value %q", sc.Value, "Wavelet") + } + + if err := InvalidateAllSystemConfigCaches(ctx); err != nil { + t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err) + } + time.Sleep(50 * time.Millisecond) // Wait for async Redis broadcast to be processed + + configs, err = ListSystemConfigsByKeys(ctx, []string{model.ConfigKeySiteName}) + if err != nil { + t.Fatalf("ListSystemConfigsByKeys(site_name) error = %v", err) + } + sc, ok = configs[model.ConfigKeySiteName] + if !ok { + t.Fatal("ListSystemConfigsByKeys(site_name) missing site_name entry after invalidate") + } + if sc.Value != "db_only_value" { + t.Fatalf("ListSystemConfigsByKeys(site_name).Value = %q, want db value %q", sc.Value, "db_only_value") } } -func TestListSystemConfigsByKeys_PopulatesRAMFromRedis(t *testing.T) { +func TestListSystemConfigsByKeys_PopulatesRAMOnMiss(t *testing.T) { _, cleanup := setupSystemConfigTest(t) defer cleanup() ctx := context.Background() @@ -125,22 +144,21 @@ func TestListSystemConfigsByKeys_PopulatesRAMFromRedis(t *testing.T) { if err := InvalidateAllSystemConfigCaches(ctx); err != nil { t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err) } - - if _, err := GetSystemConfigByKey(ctx, model.ConfigKeySiteName); err != nil { - t.Fatalf("GetSystemConfigByKey(site_name) warm error = %v", err) - } - - ResetSystemConfigRAMCacheForTest() + time.Sleep(50 * time.Millisecond) // Wait for async Redis broadcast to be processed if _, err := ListSystemConfigsByKeys(ctx, []string{model.ConfigKeySiteName}); err != nil { t.Fatalf("ListSystemConfigsByKeys(site_name) error = %v", err) } - cached, ok := systemConfigRAMCache.GetIfPresent(model.ConfigKeySiteName) + cachedItem, ok := ram.Get(ConfigCacheType, model.ConfigKeySiteName) if !ok { - t.Fatal("expected RAM cache to be populated after redis hit") + t.Fatal("expected RAM cache to be populated after config query") } - if cached.Value != "Wavelet" { - t.Fatalf("RAM cache value = %q, want %q", cached.Value, "Wavelet") + var cachedConfig model.SystemConfig + if err := json.Unmarshal([]byte(cachedItem.Value), &cachedConfig); err != nil { + t.Fatalf("unmarshal cached value error = %v", err) } -} \ No newline at end of file + if cachedConfig.Value != "Wavelet" { + t.Fatalf("RAM cache value = %q, want %q", cachedConfig.Value, "Wavelet") + } +}