From 381c79417ebf51bd634d08cd6da5ed0c10874a85 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 29 Aug 2026 08:12:27 +0800 Subject: [PATCH] autoresearch iter 5: recover panics in background cleanup loops Four long-running goroutines were launched with a bare go statement, so a panic in any of them took down the whole process: the RAM cache's expired-key eviction, the batch writer's flush worker, the disk cache cleanup worker and the PoW memory store sweeper. Route them through util.Go. The architecture gate only grepped for 'go func(', which is why the named-call form went unnoticed; widen it to cover both launch styles. --- backend/pkg/batchwriter/writer.go | 3 ++- backend/pkg/cache/ram/manager.go | 3 ++- backend/plugins/domain/cap/pow/store.go | 3 ++- backend/plugins/infra/storage/diskcache/cache.go | 3 ++- scripts/check_cordis_architecture.sh | 9 ++++++--- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backend/pkg/batchwriter/writer.go b/backend/pkg/batchwriter/writer.go index 431bbd69..46aed750 100644 --- a/backend/pkg/batchwriter/writer.go +++ b/backend/pkg/batchwriter/writer.go @@ -7,6 +7,7 @@ package batchwriter import ( + "Wavelet/pkg/util" "context" "sync" "sync/atomic" @@ -96,7 +97,7 @@ func (w *Writer[T]) Start(parent context.Context) { w.ch = make(chan T, w.cfg.QueueSize) w.workerCtx = context.WithoutCancel(parent) - go w.run() + util.Go(w.run) }) } diff --git a/backend/pkg/cache/ram/manager.go b/backend/pkg/cache/ram/manager.go index f316d88d..d961187e 100644 --- a/backend/pkg/cache/ram/manager.go +++ b/backend/pkg/cache/ram/manager.go @@ -4,6 +4,7 @@ package ram import ( + "Wavelet/pkg/util" "context" "errors" "sync" @@ -73,7 +74,7 @@ func Get(configType, key string) (CacheItem, bool) { // Check expiration if entry.item.TTL != -1 && !entry.expireAt.IsZero() && time.Now().After(entry.expireAt) { // Asynchronously remove the expired item from the map and write back - go deleteKeyIfExpired(configType, key, entry.expireAt) + util.Go(func() { deleteKeyIfExpired(configType, key, entry.expireAt) }) return CacheItem{}, false } diff --git a/backend/plugins/domain/cap/pow/store.go b/backend/plugins/domain/cap/pow/store.go index d24fdf10..4606b546 100644 --- a/backend/plugins/domain/cap/pow/store.go +++ b/backend/plugins/domain/cap/pow/store.go @@ -4,6 +4,7 @@ package pow import ( + "Wavelet/pkg/util" "context" "errors" "sync" @@ -43,7 +44,7 @@ func NewMemoryStore(cleanupInterval time.Duration) *MemoryStore { items: make(map[string]memoryItem), } if cleanupInterval > 0 { - go store.startCleanupLoop(cleanupInterval) + util.Go(func() { store.startCleanupLoop(cleanupInterval) }) } return store } diff --git a/backend/plugins/infra/storage/diskcache/cache.go b/backend/plugins/infra/storage/diskcache/cache.go index 177a87a4..744d8ba5 100644 --- a/backend/plugins/infra/storage/diskcache/cache.go +++ b/backend/plugins/infra/storage/diskcache/cache.go @@ -11,6 +11,7 @@ import ( "time" pkgcache "Wavelet/pkg/cache/disk" + "Wavelet/pkg/util" ) // Status represents the runtime cache statistics. @@ -49,7 +50,7 @@ func GetGlobalCache() *DiskCache { // Load initial configs from database globalCache.ReloadConfig(context.Background()) // Start background routine to clean expired items every 10 minutes - go globalCache.StartCleanupWorker(defaultCleanupInterval * time.Minute) + util.Go(func() { globalCache.StartCleanupWorker(defaultCleanupInterval * time.Minute) }) }) return globalCache } diff --git a/scripts/check_cordis_architecture.sh b/scripts/check_cordis_architecture.sh index 77df1624..3c040d25 100755 --- a/scripts/check_cordis_architecture.sh +++ b/scripts/check_cordis_architecture.sh @@ -182,13 +182,16 @@ fi # ============================================================================== # 6. 并发安全规范 (Goroutine Concurrency Safety) # ============================================================================== -log_check "6. 检查并发安全规范 (禁止生产代码中使用裸 go func())..." +log_check "6. 检查并发安全规范 (禁止生产代码中使用裸 go 启动 goroutine)..." -BARE_GO_ROUTINES=$(rg -n '\bgo func\(' "${BACKEND_DIR}" \ +# 同时覆盖 `go func() {...}()` 匿名形式与 `go worker.run()` / `go loop()` 命名调用形式: +# 两者都不具备 panic 恢复能力,被调方一旦 panic 会直接击穿整个进程。 +# 例外:util.Go 自身的实现与事件总线 (它们内部已 recover)。 +BARE_GO_ROUTINES=$(rg -n --pcre2 '^[[:space:]]*go\s+(func\s*[\w{]|\w+(\.\w+)*\s*[\({])' "${BACKEND_DIR}" \ --glob '*.go' -g '!*_test.go' -g '!goroutine.go' -g '!events.go' || true) if [ -n "${BARE_GO_ROUTINES}" ]; then - log_fail "生产代码严禁使用裸 'go func()',必须使用 'util.Go' 确保 panic 恢复与调用栈追踪:" + log_fail "生产代码严禁裸 'go' 启动 goroutine(含 'go func()' 与 'go xxx()' 命名调用),必须使用 'util.Go' 确保 panic 恢复与调用栈追踪:" echo "${BARE_GO_ROUTINES}" >&2 else log_pass "并发调用统一使用 util.Go 具备 panic 恢复能力"