From 6e12a7fd5dc75760293c6d459bd3fe478becf19d Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 28 Aug 2026 16:50:03 +0800 Subject: [PATCH] fix(admin): propagate cache errors in FlushTaskExecutionLog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缓存故障(非 ErrCacheMiss)不再被静默吞掉:上抛包装错误,避免缓冲任务日志丢失并误报持久化成功; ErrCacheMiss 仍视为无日志的正常路径。附 3 个回归测试(故障/未命中/持久化+清理)。 lint_issues 34→33 (nilerr 清零) --- backend/plugins/domain/admin/repository.go | 11 +- .../domain/admin/repository_flush_log_test.go | 113 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 backend/plugins/domain/admin/repository_flush_log_test.go diff --git a/backend/plugins/domain/admin/repository.go b/backend/plugins/domain/admin/repository.go index 7e630448..39349a49 100644 --- a/backend/plugins/domain/admin/repository.go +++ b/backend/plugins/domain/admin/repository.go @@ -4,6 +4,7 @@ package admin import ( + "Wavelet/core/contracts" "Wavelet/pkg/cache/ram" "Wavelet/pkg/idgen" "Wavelet/pkg/util" @@ -502,7 +503,15 @@ func FlushTaskExecutionLog(ctx context.Context, taskID string) error { key := taskExecutionLogRedisKey(taskID) var logText string - if err := cacheSvc.Get(ctx, key, &logText); err != nil || logText == "" { + if err := cacheSvc.Get(ctx, key, &logText); err != nil { + // 缓存未命中属于正常情况(任务无输出),其余错误必须上抛, + // 否则缓冲日志会被静默丢弃并误报持久化成功。 + if !errors.Is(err, contracts.ErrCacheMiss) { + return fmt.Errorf("load buffered task execution log: %w", err) + } + return nil + } + if logText == "" { return nil } diff --git a/backend/plugins/domain/admin/repository_flush_log_test.go b/backend/plugins/domain/admin/repository_flush_log_test.go new file mode 100644 index 00000000..57415b54 --- /dev/null +++ b/backend/plugins/domain/admin/repository_flush_log_test.go @@ -0,0 +1,113 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package admin + +import ( + "context" + "errors" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/redis/go-redis/v9" + "github.com/redis/go-redis/v9/maintnotifications" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" + + "Wavelet/core" + "Wavelet/core/contracts" + cacheplugin "Wavelet/plugins/infra/cache" +) + +// stubDBService 用内存 SQLite 满足 DBService 契约,隔离外部依赖。 +type stubDBService struct{ db *gorm.DB } + +func (s stubDBService) GORM() *gorm.DB { return s.db } + +func (s stubDBService) DB(context.Context) *gorm.DB { return s.db } + +func (s stubDBService) Named(string) *gorm.DB { return s.db } + +// newFlushLogTestCache 构建真实多层缓存服务并注入 admin 插件上下文。 +func newFlushLogTestCache(t *testing.T) (contracts.CacheService, *miniredis.Miniredis, func()) { + t.Helper() + mr, err := miniredis.Run() + require.NoError(t, err) + rdb := redis.NewClient(&redis.Options{Addr: mr.Addr(), MaintNotificationsConfig: &maintnotifications.Config{Mode: maintnotifications.ModeDisabled}}) + + p := cacheplugin.New(cacheplugin.WithRedis(rdb), cacheplugin.WithRAMCapacity(64)) + ctx := core.NewContext(context.Background()) + require.NoError(t, p.Apply(ctx)) + svc, err := core.Inject[contracts.CacheService](ctx) + require.NoError(t, err) + + prev := cacheService + SetCacheService(svc) + cleanup := func() { + SetCacheService(prev) + _ = rdb.Close() + mr.Close() + } + return svc, mr, cleanup +} + +// TestFlushTaskExecutionLogPropagatesCacheError 回归:缓存读取失败(非未命中)时, +// FlushTaskExecutionLog 必须返回错误而不是静默吞掉日志并误报成功(nilerr 修复)。 +func TestFlushTaskExecutionLogPropagatesCacheError(t *testing.T) { + _, mr, cleanup := newFlushLogTestCache(t) + defer cleanup() + + ctx := context.Background() + const taskID = "flush-err-task" + + // 先缓冲一行日志 + require.NoError(t, AppendTaskExecutionLog(ctx, taskID, "step-1 ok")) + + // 关闭 miniredis 模拟缓存基础设施故障(读取出错而非未命中) + mr.Close() + + err := FlushTaskExecutionLog(ctx, taskID) + assert.Error(t, err, "缓存故障时必须返回错误,防止缓冲日志被静默丢弃") +} + +// TestFlushTaskExecutionLogCacheMissIsNoop 回归:任务无缓冲日志(未命中)时应为空操作成功。 +func TestFlushTaskExecutionLogCacheMissIsNoop(t *testing.T) { + _, _, cleanup := newFlushLogTestCache(t) + defer cleanup() + + ctx := context.Background() + assert.NoError(t, FlushTaskExecutionLog(ctx, "missing-task")) +} + +// TestFlushTaskExecutionLogPersistsAndClears 验证正常路径:缓冲日志写入执行记录后清理缓存。 +func TestFlushTaskExecutionLogPersistsAndClears(t *testing.T) { + svc, _, cleanup := newFlushLogTestCache(t) + defer cleanup() + + ctx := context.Background() + const taskID = "flush-ok-task" + require.NoError(t, AppendTaskExecutionLog(ctx, taskID, "done")) + + sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, sqliteDB.AutoMigrate(&TaskExecution{})) + SetDBService(stubDBService{db: sqliteDB}) + defer SetDBService(nil) + gormDB := sqliteDB + exec := &TaskExecution{TaskID: taskID, TaskType: "upload:test", TaskName: "t", Status: TaskExecutionStatusSucceeded} + require.NoError(t, gormDB.Create(exec).Error) + + require.NoError(t, FlushTaskExecutionLog(ctx, taskID)) + + var got TaskExecution + require.NoError(t, gormDB.First(&got, exec.ID).Error) + assert.Contains(t, got.Log, "done") + + // 缓存中的缓冲日志应已被清理 + var buf string + err = svc.Get(ctx, taskExecutionLogRedisKey(taskID), &buf) + assert.True(t, errors.Is(err, contracts.ErrCacheMiss), "flush 后缓存应清空, got %v", err) +}