Files
xlgo-core/app_trace_test.go
杭州明婳科技 96ce883d41 feat: cron/storage/cache/ratelimit 实例化 + trace 纳入 App 生命周期 (v1.4.0)
完成 instance+facade 一致性收尾,闭合三个原始问题:
- cron/storage/cache/ratelimit 实例化(App 持 per-App 实例 + SwapDefault* 提升全局默认
  + 包级 facade 代理,照 database.Manager/jwt.Manager 模式),消除单进程多 App 互相污染。
- trace 纳入 App 生命周期(WithTrace + doInit trace.Init + doShutdown trace.Close +
  config.TraceConfig + 自动装 Middleware)。不做实例隔离(OTel 进程全局)。
- 删除 WithoutWire 空函数技术债。

附带修复:
- cache/ratelimit redis 注入(jwt 模型:注入优先、database.GetRedis() 兜底)。
- commitReplacedResources 不再关闭 previousDB/previousRedis/loggerSnapshot
  (多 App 下 previous 可能是另一 App 的活跃资源,关闭致 redis: client is closed)。
- app.RateLimitRegistry() + App-bound 限流中间件(多 App per-App 计数隔离)。
- StorageManager.Close() 闭合 Init/Close 对称性(io.Closer 断言,预留扩展点),
  closeResources 与 rollbackReplacedResources 对称收口。

新增公开 API:WithTrace / WithCronTask;app.Scheduler()/Cache()/RedisClient()/
RateLimitRegistry();cron/storage/cache/middleware 各 SwapDefault*;config.TraceConfig;
middleware.RateLimitRegistry + App-bound 方法;middleware.WithRedisClient;
storage.StorageManager.Close。

Breaking:删 WithoutWire;cron.AddTask pre-Init 注册失效(改用 WithCronTask);
middleware.StopRateLimiters 语义收窄;commitReplacedResources 不再关 previous;
cache.NewRedisCacheWithRedis / CacheManager.InitWithRedis 等签名变更。
详见 CHANGELOG [1.4.0] 升级说明。

验证:go build + go vet + go test -race ./... 全绿(-buildvcs=false)。
独立对抗性复审无 CRITICAL/HIGH。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:25:37 +08:00

121 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package xlgo_test
import (
"context"
"net/http"
"net/http/httptest"
"runtime"
"strings"
"testing"
"time"
xlgo "github.com/EthanCodeCraft/xlgo-core"
"github.com/EthanCodeCraft/xlgo-core/trace"
"github.com/gin-gonic/gin"
)
// TestWithTraceInitErrorPropagated 固化 Phase 1 契约:WithTrace 启用后 doInit 必须调
// trace.Init 并把错误向上传播。用非法 ExporterType 触发 trace.Init 失败(config.Validate
// 只校验 SampleRatio,故 bogus exporter 能过 Validate、在 trace.Init 失败,证明是 App 主动调 Init)。
func TestWithTraceInitErrorPropagated(t *testing.T) {
cfg := testConfig(18090)
cfg.Trace.Enabled = true
cfg.Trace.ExporterType = "bogus-exporter"
app := xlgo.New(xlgo.WithConfig(cfg), xlgo.WithTrace())
err := app.Init()
if err == nil {
_ = app.Shutdown()
t.Fatal("expected Init to fail with bogus trace exporter, got nil")
}
// "初始化 trace 失败" 包裹 trace.Init 的 "不支持的导出器类型"
msg := err.Error()
if !strings.Contains(msg, "trace") && !strings.Contains(msg, "导出器") {
t.Fatalf("expected trace init error, got %v", err)
}
}
// TestWithTraceNoopMiddlewareDoesNotBreakRequest 固化:WithTrace + Enabled=false(默认)
// 时 trace.Init 安装 Noop tracertrace.Middleware 装入链后不应破坏正常请求。
func TestWithTraceNoopMiddlewareDoesNotBreakRequest(t *testing.T) {
cfg := testConfig(18091)
// Trace.Enabled 保持默认 false -> noop
app := xlgo.New(xlgo.WithConfig(cfg), xlgo.WithTrace())
if err := app.Init(); err != nil {
t.Fatalf("Init: %v", err)
}
defer app.Shutdown()
// Init 后(middleware 链已装入)注册路由,确保该路由经过 trace 中间件。
app.GetRouter().GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
app.GetRouter().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("trace noop middleware broke request: got status %d, body %s", w.Code, w.Body.String())
}
}
// TestWithTraceShutdownReleasesExporterGoroutine 固化 Phase 1 生命周期闭环:
// WithTrace + Enabled=true + stdout exporterInit 后 BatchSpanProcessor 后台 goroutine
// 在运行;Shutdown 必须调 trace.Close 让其退出,否则 goroutine 泄漏(H-14 修了 Close 幂等,
// 但 App 此前从未调 Close -- 本测试断言 App.Shutdown 确实调了)。
//
// 不发任何请求 -> stdout exporter 不输出 -> 无测试输出污染;仅观测 goroutine 计数。
func TestWithTraceShutdownReleasesExporterGoroutine(t *testing.T) {
cfg := testConfig(18092)
cfg.Trace.Enabled = true
cfg.Trace.ExporterType = "stdout"
app := xlgo.New(xlgo.WithConfig(cfg), xlgo.WithTrace())
baseline := runtime.NumGoroutine()
if err := app.Init(); err != nil {
t.Fatalf("Init: %v", err)
}
afterInit := runtime.NumGoroutine()
if afterInit <= baseline {
t.Logf("note: afterInit=%d baseline=%d (batch goroutine delta not detected; test may be noisy)", afterInit, baseline)
}
if err := app.Shutdown(); err != nil {
t.Fatalf("Shutdown: %v", err)
}
// trace.Close 的 provider.Shutdown 异步停止 batch goroutine,轮询等待回归 baseline。
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if runtime.NumGoroutine() <= baseline {
break
}
time.Sleep(20 * time.Millisecond)
}
if after := runtime.NumGoroutine(); after > baseline {
t.Errorf("trace.Close 未释放 exporter goroutineApp.Shutdown 疑未调 trace.Close: baseline=%d after=%d", baseline, after)
}
}
// TestWithTraceEnabledStdoutInitOK 固化:WithTrace + Enabled=true + stdout 是合法组合,
// trace.Init 成功,App.Init 成功;二次 trace.Close 幂等(H-14)。
func TestWithTraceEnabledStdoutInitOK(t *testing.T) {
cfg := testConfig(18093)
cfg.Trace.Enabled = true
cfg.Trace.ExporterType = "stdout"
app := xlgo.New(xlgo.WithConfig(cfg), xlgo.WithTrace())
if err := app.Init(); err != nil {
t.Fatalf("Init: %v", err)
}
if err := app.Shutdown(); err != nil {
t.Fatalf("Shutdown: %v", err)
}
// Shutdown 已调 trace.Close;再调一次必须幂等不报错(H-14 契约)
if err := trace.Close(context.Background()); err != nil {
t.Fatalf("trace.Close after Shutdown should be idempotent: %v", err)
}
}