Skip to content

Commit 31cc56a

Browse files
xueyizhengclaude
andcommitted
[test][evaluation] add goroutine-leak test at callEvaluators early-return path
Adds a regression test at the actual leak site (callEvaluators via the public CallEvaluators), not just the low-level pool. It drives the "evaluator conf not found" early return (which skips pool.ExecAll) 50 times and asserts runtime.NumGoroutine does not grow. Verified the test catches the bug: with the fix disabled (defer pool.Release() removed) it fails with delta=100 (2 leaked resident goroutines × 50 calls); with the fix it passes (delta≈0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e308438 commit 31cc56a

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

‎backend/modules/evaluation/domain/service/expt_run_item_turn_impl_test.go‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package service
55
import (
66
"context"
77
"errors"
8+
"runtime"
89
"strconv"
910
"sync"
1011
"testing"
@@ -4292,3 +4293,92 @@ func TestDefaultExptTurnEvaluationImpl_callTarget_CustomAgentAndA2AAgent(t *test
42924293
})
42934294
}
42944295
}
4296+
4297+
// TestDefaultExptTurnEvaluationImpl_CallEvaluators_NoGoroutineLeak 在真实泄漏点 callEvaluators 直接验证:
4298+
// 当 evaluator conf 缺失导致中途提前 return(跳过 pool.ExecAll)时,协程池仍被释放,不泄漏 goroutine。
4299+
// 修复前(NewPool 后无 defer pool.Release()),每次调用会泄漏底层 ants pool 的 2 个常驻协程(purge/ticktock),
4300+
// 此测试会因协程数持续增长而失败;修复后应保持平稳。
4301+
func TestDefaultExptTurnEvaluationImpl_CallEvaluators_NoGoroutineLeak(t *testing.T) {
4302+
ctrl := gomock.NewController(t)
4303+
defer ctrl.Finish()
4304+
4305+
// evalTargetService 在提前 return 前不会被调用到(OutputFields 无 omitted 大字段),这里可留空 mock。
4306+
// benefitService 在 callEvaluators 之前的权益校验会被调用,mock 成功放行。
4307+
mockBenefit := benefitmocks.NewMockIBenefitService(ctrl)
4308+
mockBenefit.EXPECT().CheckAndDeductEvalBenefit(gomock.Any(), gomock.Any()).
4309+
Return(&benefit.CheckAndDeductEvalBenefitResult{}, nil).AnyTimes()
4310+
service := &DefaultExptTurnEvaluationImpl{
4311+
evalTargetService: svcmocks.NewMockIEvalTargetService(ctrl),
4312+
benefitService: mockBenefit,
4313+
metric: metricsmocks.NewMockExptMetric(ctrl),
4314+
}
4315+
4316+
target := &entity.EvalTargetRecord{
4317+
EvalTargetOutputData: &entity.EvalTargetOutputData{
4318+
OutputFields: map[string]*entity.Content{
4319+
"field1": {Text: gptr.Of("v")},
4320+
},
4321+
},
4322+
}
4323+
4324+
// 构造一个会触发 "evaluator conf not found" 提前 return 的 etec:
4325+
// Expt.Evaluators 里有 versionID=1 的 evaluator,但 EvaluatorsConf.EvaluatorConf 为空 → GetEvaluatorConf(1)=nil。
4326+
newEtec := func() *entity.ExptTurnEvalCtx {
4327+
return &entity.ExptTurnEvalCtx{
4328+
ExptItemEvalCtx: &entity.ExptItemEvalCtx{
4329+
EvalSetItem: &entity.EvaluationSetItem{ID: 1, ItemID: 2},
4330+
Event: &entity.ExptItemEvalEvent{
4331+
Session: &entity.Session{UserID: "u"},
4332+
ExptID: 1,
4333+
SpaceID: 2,
4334+
},
4335+
Expt: &entity.Experiment{
4336+
ID: 1,
4337+
SpaceID: 2,
4338+
Evaluators: []*entity.Evaluator{
4339+
{
4340+
ID: 1,
4341+
EvaluatorType: entity.EvaluatorTypePrompt,
4342+
PromptEvaluatorVersion: &entity.PromptEvaluatorVersion{ID: 1},
4343+
},
4344+
},
4345+
EvalConf: &entity.EvaluationConfiguration{
4346+
ItemConcurNum: gptr.Of(1),
4347+
ConnectorConf: entity.Connector{
4348+
EvaluatorsConf: &entity.EvaluatorsConf{
4349+
EvaluatorConcurNum: gptr.Of(1),
4350+
// 故意不放 versionID=1 的 conf → 触发 :433 提前 return
4351+
EvaluatorConf: []*entity.EvaluatorConf{},
4352+
},
4353+
},
4354+
},
4355+
},
4356+
},
4357+
ExptTurnRunResult: &entity.ExptTurnRunResult{},
4358+
Turn: &entity.Turn{FieldDataList: []*entity.FieldData{}},
4359+
}
4360+
}
4361+
4362+
// 先跑一次预热(确保触发的是提前 return 路径),并让运行时协程稳定。
4363+
if _, err := service.CallEvaluators(context.Background(), newEtec(), target); err == nil {
4364+
t.Fatalf("expected evaluator-conf-not-found error to hit the early-return path")
4365+
}
4366+
time.Sleep(200 * time.Millisecond)
4367+
runtime.GC()
4368+
before := runtime.NumGoroutine()
4369+
4370+
const N = 50
4371+
for i := 0; i < N; i++ {
4372+
_, err := service.CallEvaluators(context.Background(), newEtec(), target)
4373+
assert.Error(t, err) // 每次都走 conf-not-found 提前 return
4374+
}
4375+
4376+
// 等底层 ants pool 的常驻协程随 Release 退出。
4377+
time.Sleep(500 * time.Millisecond)
4378+
runtime.GC()
4379+
after := runtime.NumGoroutine()
4380+
4381+
// 修复前:N 次泄漏 ≈ 2N=100 个常驻协程;修复后应基本持平,留少量裕度。
4382+
assert.Less(t, after-before, 20,
4383+
"goroutine leak in callEvaluators early-return path: before=%d after=%d (delta=%d)", before, after, after-before)
4384+
}

0 commit comments

Comments
 (0)