后台 goroutine panic 防护:新增 pkg/util.Go 共享助手(recover+调用点日志),全仓 22 个裸 go func() 站点统一收口

Result: {"status":"keep","total_issues":8,"eslint_errors":0,"eslint_problems":0,"eslint_warnings":0,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_total":0,"golint_test_usetesting":0,"golint_total":8,"golint_usestdlibvars":0,"golint_vetx_total":0,"golint_wastedassign":0,"measure_s":112,"tsc_errors":0,"vitest_failed":0,"vitest_total":126}
This commit is contained in:
ryan
2026-08-26 11:06:23 +08:00
parent 3528323b50
commit 55db1c01ec
22 changed files with 137 additions and 54 deletions
+3 -2
View File
@@ -18,6 +18,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/repository/logstore"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/Rain-kl/Wavelet/pkg/util"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/shared/response"
@@ -106,7 +107,7 @@ func HandleLogWebSocket(c *gin.Context) {
// 在独立 goroutine 中读取客户端消息(保持连接活跃 + 检测断开)
done := make(chan struct{})
go func() {
util.Go(func() {
defer close(done)
for {
_, _, err := conn.ReadMessage()
@@ -114,7 +115,7 @@ func HandleLogWebSocket(c *gin.Context) {
return
}
}
}()
})
// 主循环:推送日志
for {
+3 -2
View File
@@ -17,6 +17,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
pkgpush "github.com/Rain-kl/Wavelet/pkg/push"
"github.com/Rain-kl/Wavelet/pkg/util"
"gorm.io/gorm"
)
@@ -75,7 +76,7 @@ var DefaultTrigger = &EventTrigger{}
//nolint:contextcheck
func (t *EventTrigger) Trigger(ctx context.Context, meta EventMetadata, body map[string]any) {
asyncCtx := context.WithoutCancel(ctx)
go func() {
util.Go(func() {
if body == nil {
body = make(map[string]any)
}
@@ -99,7 +100,7 @@ func (t *EventTrigger) Trigger(ctx context.Context, meta EventMetadata, body map
flatBody := getFlatBody(body)
msg, _ := t.buildMessage(&event, meta, flatBody, body)
t.enqueuePushTasks(asyncCtx, meta, &event, msg, flatBody)
}()
})
}
func (t *EventTrigger) buildMessage(event *model.PushEvent, meta EventMetadata, flatBody map[string]any, body map[string]any) (NotificationMessage, string) {
+3 -2
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/Rain-kl/Wavelet/pkg/util"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/shared/response"
@@ -58,11 +59,11 @@ func ApplyUpdate(c *gin.Context) {
logger.InfoF(c.Request.Context(), "[Updater] upgrade prepared; restarting with %s", stagedBinary)
c.JSON(http.StatusOK, response.OKNil())
go func() {
util.Go(func() {
time.Sleep(time.Second)
if err := replaceAndRestart(executable, stagedBinary); err != nil {
defaultManager.finishUpgrade()
logger.ErrorF(context.Background(), "[Updater] replace and restart failed: %v", err)
}
}()
})
}
+5 -4
View File
@@ -18,6 +18,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
"github.com/Rain-kl/Wavelet/internal/apps/agent/wsclient"
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
"github.com/Rain-kl/Wavelet/pkg/util"
)
// HeartbeatService handles node registration and periodic heartbeat reporting.
@@ -200,12 +201,12 @@ func (r *Runner) startWebSocket(ctx context.Context, nodeID string) (<-chan erro
return nil, err
}
done := make(chan error, 1)
go func() {
util.Go(func() {
defer func() {
_ = conn.Close()
}()
done <- r.runWebSocket(ctx, nodeID, conn)
}()
})
return done, nil
}
@@ -251,7 +252,7 @@ func (r *Runner) runWebSocket(ctx context.Context, nodeID string, conn protocol.
childCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
util.Go(func() {
for {
select {
case <-childCtx.Done():
@@ -264,7 +265,7 @@ func (r *Runner) runWebSocket(ctx context.Context, nodeID string, conn protocol.
}
}
}
}()
})
wsConn, ok := conn.(*wsclient.Connection)
if !ok {
+3 -2
View File
@@ -17,6 +17,7 @@ import (
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const (
@@ -190,7 +191,7 @@ func startRuntimeSettingsInvalidationListener() {
return
}
go func() {
util.Go(func() {
pubsub := db.Redis.Subscribe(context.Background(), repository.SystemConfigInvalidationChannel)
defer func() {
_ = pubsub.Close()
@@ -208,5 +209,5 @@ func startRuntimeSettingsInvalidationListener() {
InvalidateRuntimeSettings()
}
}
}()
})
}
+3 -2
View File
@@ -22,6 +22,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
service "github.com/Rain-kl/Wavelet/pkg/protocol"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const (
@@ -180,7 +181,7 @@ func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath
}
m.processes[relayID] = proc
go func() {
util.Go(func() {
backoff := 1 * time.Second
const maxBackoff = 60 * time.Second
@@ -262,7 +263,7 @@ func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath
}
t.Stop()
}
}()
})
}
// Stop cancels and removes all managed frpc processes.
+9 -8
View File
@@ -14,6 +14,7 @@ import (
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const (
@@ -60,7 +61,7 @@ func startTokenCacheInvalidationListener() {
tokenListenerDone = make(chan struct{})
redisClient := db.Redis // 捕获当前客户端:goroutine 不读可变全局,避免与测试置空 db.Redis 竞争
go func() {
util.Go(func() {
listenerCtx := tokenListenerCtx
defer close(tokenListenerDone)
@@ -69,10 +70,10 @@ func startTokenCacheInvalidationListener() {
_ = pubsub.Close()
}()
go func() {
util.Go(func() {
<-listenerCtx.Done()
_ = pubsub.Close()
}()
})
for msg := range pubsub.Channel() {
tokenHash := msg.Payload
@@ -82,7 +83,7 @@ func startTokenCacheInvalidationListener() {
tokenRAM.Invalidate(tokenHash)
}
}
}()
})
}
func publishTokenRAMInvalidation(ctx context.Context, tokenHash string) {
@@ -104,7 +105,7 @@ func startUserCacheInvalidationListener() {
userListenerDone = make(chan struct{})
redisClient := db.Redis // 捕获当前客户端:goroutine 不读可变全局,避免与测试置空 db.Redis 竞争
go func() {
util.Go(func() {
listenerCtx := userListenerCtx
defer close(userListenerDone)
@@ -113,10 +114,10 @@ func startUserCacheInvalidationListener() {
_ = pubsub.Close()
}()
go func() {
util.Go(func() {
<-listenerCtx.Done()
_ = pubsub.Close()
}()
})
for msg := range pubsub.Channel() {
userIDStr := msg.Payload
@@ -126,7 +127,7 @@ func startUserCacheInvalidationListener() {
userRAM.Invalidate(userID)
}
}
}()
})
}
func publishUserRAMInvalidation(ctx context.Context, userID uint64) {
+3 -2
View File
@@ -20,6 +20,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/Rain-kl/Wavelet/pkg/util"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -91,7 +92,7 @@ func startSourceLeaseHeartbeat(
workCtx, cancel := context.WithCancel(ctx)
done := make(chan error, 1)
heartbeat := &sourceLeaseHeartbeat{cancel: cancel, done: done}
go func() {
util.Go(func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
@@ -117,7 +118,7 @@ func startSourceLeaseHeartbeat(
}
}
}
}()
})
return workCtx, heartbeat, nil
}
+3 -2
View File
@@ -17,6 +17,7 @@ import (
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const fileAccessInvalidationChannel = "upload:file_access_invalidation"
@@ -57,7 +58,7 @@ func startAccessCacheInvalidationListener() {
return
}
go func() {
util.Go(func() {
pubsub := redis.Subscribe(
context.Background(),
objectstore.ConfigInvalidationChannel,
@@ -70,7 +71,7 @@ func startAccessCacheInvalidationListener() {
for range pubsub.Channel() {
ResetAccessCaches()
}
}()
})
}
// IsFilePublic reports whether uploadType is in the public access whitelist.
+5 -4
View File
@@ -13,6 +13,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const (
@@ -54,17 +55,17 @@ func startUploadMetaCacheInvalidationListener() {
// 捕获当前客户端:goroutine 不再读可变全局 db.Redis,测试置空/替换全局时不会数据竞争
redisClient := db.Redis
go func() {
util.Go(func() {
defer close(uploadMetaListenerDone)
pubsub := redisClient.Subscribe(uploadMetaListenerCtx, uploadMetaInvalidationChan)
defer func() {
_ = pubsub.Close()
}()
go func() {
util.Go(func() {
<-uploadMetaListenerCtx.Done()
_ = pubsub.Close()
}()
})
for msg := range pubsub.Channel() {
var payload uploadMetaInvalidationMessage
@@ -74,7 +75,7 @@ func startUploadMetaCacheInvalidationListener() {
}
uploadMetaRAM.Invalidate(payload.ID)
}
}()
})
}
func publishUploadMetaRAMInvalidation(ctx context.Context, id uint64) {
@@ -24,6 +24,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/infra/task"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const (
@@ -101,7 +102,7 @@ func (h *MigrationHandler) Execute(ctx context.Context, payload []byte) (*task.T
}()
//nolint:contextcheck,gosec
go func() {
util.Go(func() {
ticker := time.NewTicker(renewalInterval)
defer ticker.Stop()
for {
@@ -116,7 +117,7 @@ func (h *MigrationHandler) Execute(ctx context.Context, payload []byte) (*task.T
return
}
}
}()
})
}
active, err := objectstore.LoadConfig(ctx)
+3 -2
View File
@@ -16,6 +16,7 @@ import (
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/util"
"gorm.io/gorm"
)
@@ -86,7 +87,7 @@ func startPubSubListener() {
if db.Redis == nil {
return
}
go func() {
util.Go(func() {
pubsub := db.Redis.Subscribe(context.Background(), ConfigInvalidationChannel)
defer func() {
_ = pubsub.Close()
@@ -96,7 +97,7 @@ func startPubSubListener() {
for range ch {
ResetCache()
}
}()
})
}
// Active returns the configured active driver and backend, using an in-memory cache with 5s TTL.
+4 -2
View File
@@ -8,6 +8,8 @@ import (
"context"
"log"
"sync"
"github.com/Rain-kl/Wavelet/pkg/util"
)
// ShutdownFunc defines the signature for a graceful shutdown callback.
@@ -52,10 +54,10 @@ func Stop(ctx context.Context) {
}
done := make(chan struct{})
go func() {
util.Go(func() {
wg.Wait()
close(done)
}()
})
select {
case <-done:
+5 -4
View File
@@ -13,6 +13,7 @@ import (
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const (
@@ -120,7 +121,7 @@ func startAuthSourceCacheInvalidationListener() {
authSourceListenerDone = make(chan struct{})
redisClient := db.Redis // 捕获当前客户端:goroutine 不读可变全局,避免与测试置空 db.Redis 竞争
go func() {
util.Go(func() {
listenerCtx := authSourceListenerCtx
defer close(authSourceListenerDone)
@@ -129,16 +130,16 @@ func startAuthSourceCacheInvalidationListener() {
_ = pubsub.Close()
}()
go func() {
util.Go(func() {
<-listenerCtx.Done()
_ = pubsub.Close()
}()
})
for range pubsub.Channel() {
authSourceActiveRAM.InvalidateAll()
authSourceByNameRAM.InvalidateAll()
}
}()
})
}
func publishAuthSourceRAMInvalidation(ctx context.Context) {
+5 -4
View File
@@ -14,6 +14,7 @@ import (
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
"github.com/Rain-kl/Wavelet/pkg/util"
)
const (
@@ -106,7 +107,7 @@ func startSystemConfigCacheInvalidationListener() {
systemConfigListenerDone = make(chan struct{})
redisClient := db.Redis // 捕获当前客户端:goroutine 不读可变全局,避免与测试置空 db.Redis 竞争
go func() {
util.Go(func() {
listenerCtx := systemConfigListenerCtx
defer close(systemConfigListenerDone)
@@ -115,10 +116,10 @@ func startSystemConfigCacheInvalidationListener() {
_ = pubsub.Close()
}()
go func() {
util.Go(func() {
<-listenerCtx.Done()
_ = pubsub.Close()
}()
})
for msg := range pubsub.Channel() {
var payload systemConfigBroadcastMessage
@@ -134,7 +135,7 @@ func startSystemConfigCacheInvalidationListener() {
ram.Delete(payload.Type, key)
}
}
}()
})
}
// StopSystemConfigCacheListener stops the Redis Pub/Sub subscription listener and resets the sync.Once guard.
+3 -2
View File
@@ -23,6 +23,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/infra/config"
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
"github.com/Rain-kl/Wavelet/pkg/util"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
@@ -93,12 +94,12 @@ func Serve(onStarted func()) {
onStarted()
}
go func() {
util.Go(func() {
log.Printf("[API] server listening on %s\n", config.Config.App.Addr)
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("[API] server failed: %v\n", err)
}
}()
})
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)