mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-12 22:55:45 +08:00
### Summary Adopt sync.WaitGroup.Go (Go 1.25) to simplify tracked goroutine spawning. This replaces the error-prone trio of wg.Add(1), go func(), and defer wg.Done() with a single, self-contained call. More info: https://github.com/golang/go/issues/63796 Signed-off-by: grandpig <grandpig@outlook.com>
405 lines
10 KiB
Go
405 lines
10 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"ragflow/internal/harness/core/schema"
|
|
)
|
|
|
|
// ================================================================
|
|
// Concurrency tests for agentcore concurrent components
|
|
// ================================================================
|
|
|
|
// ---- tools_node.go: concurrent tool execution via mockTool ----
|
|
|
|
// TestToolsNode_ConcurrentInvoke verifies concurrent tool Invoke calls are safe.
|
|
func TestToolsNode_ConcurrentInvoke(t *testing.T) {
|
|
tool := &mockTool{name: "conc_tool", desc: "concurrency test tool"}
|
|
ctx := context.Background()
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 20)
|
|
|
|
for i := 0; i < 20; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
_, err := tool.Invoke(ctx, `{"id":`+string(rune('0'+id%10))+`}`)
|
|
errs <- err
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Errorf("concurrent tool invoke failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- workflow.go: parallel sub-agent execution ----
|
|
|
|
// TestWorkflow_ParallelAgentConcurrency verifies parallel sub-agents
|
|
// run safely when invoked concurrently.
|
|
func TestWorkflow_ParallelAgentConcurrency(t *testing.T) {
|
|
m1 := &mockModel{}
|
|
for i := 0; i < 10; i++ {
|
|
m1.addResp("par_a_result")
|
|
}
|
|
m2 := &mockModel{}
|
|
for i := 0; i < 10; i++ {
|
|
m2.addResp("par_b_result")
|
|
}
|
|
|
|
a1 := NewReActAgent(&ReActConfig[*schema.Message]{Model: m1}).WithName("par_conc_a")
|
|
a2 := NewReActAgent(&ReActConfig[*schema.Message]{Model: m2}).WithName("par_conc_b")
|
|
|
|
ctx := context.Background()
|
|
par, err := NewParallel(ctx, &ParallelConfig{
|
|
Name: "par_conc_test",
|
|
SubAgents: []Agent{a1, a2},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewParallel: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 5)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
runner := NewTypedRunner(RunnerConfig[*schema.Message]{Agent: par})
|
|
iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("parallel conc test")})
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Err != nil {
|
|
errs <- ev.Err
|
|
return
|
|
}
|
|
}
|
|
errs <- nil
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Errorf("parallel workflow error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- AgentLoop concurrent Push/Stop ----
|
|
|
|
// TestTurnLoop_ConcurrentPushStop verifies AgentLoop handles concurrent
|
|
// Push and Stop operations safely.
|
|
func TestTurnLoop_ConcurrentPushStop(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
loop := NewAgentLoop[*schema.Message](AgentLoopConfig[*schema.Message]{
|
|
GenInput: func(_ context.Context, l *AgentLoop[*schema.Message], items []*schema.Message) (*GenInputResult[*schema.Message], error) {
|
|
return &GenInputResult[*schema.Message]{
|
|
Input: &AgentInput{Messages: items},
|
|
Consumed: items,
|
|
Remaining: nil,
|
|
}, nil
|
|
},
|
|
PrepareAgent: func(_ context.Context, _ *AgentLoop[*schema.Message], consumed []*schema.Message) (Agent, error) {
|
|
m := &mockModel{}
|
|
m.addResp("turn loop conc response")
|
|
return NewReActAgent(&ReActConfig[*schema.Message]{Model: m}).WithName("conc_loop"), nil
|
|
},
|
|
})
|
|
|
|
var wg sync.WaitGroup
|
|
// Concurrent Push from multiple goroutines
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
loop.Push(schema.UserMessage("concurrent item"))
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
loop.Run(ctx)
|
|
loop.Stop()
|
|
state := loop.Wait()
|
|
if state.ExitReason != nil && !errors.As(state.ExitReason, new(*CancelError)) {
|
|
t.Logf("turn loop exit: %v", state.ExitReason)
|
|
}
|
|
}
|
|
|
|
// ---- ReActAgent concurrent Run ----
|
|
|
|
// TestReActAgent_ConcurrentRun verifies multiple agents can run concurrently.
|
|
func TestReActAgent_ConcurrentRun(t *testing.T) {
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 10)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
m := &mockModel{}
|
|
m.addResp("concurrent result")
|
|
m.addResp("concurrent result")
|
|
agent := NewReActAgent(&ReActConfig[*schema.Message]{Model: m}).WithName("conc_agent")
|
|
iter := agent.Run(ctx, &AgentInput{
|
|
Messages: []*schema.Message{schema.UserMessage("concurrent run test")},
|
|
})
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Err != nil {
|
|
errs <- ev.Err
|
|
return
|
|
}
|
|
}
|
|
errs <- nil
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Errorf("concurrent agent run failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Runner concurrent execution ----
|
|
|
|
// TestRunner_ConcurrentInstances verifies multiple Runner instances
|
|
// executing concurrently don't interfere.
|
|
func TestRunner_ConcurrentInstances(t *testing.T) {
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 8)
|
|
|
|
for i := 0; i < 8; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
m := &mockModel{}
|
|
m.addResp("runner conc result")
|
|
m.addResp("runner conc result")
|
|
agent := NewReActAgent(&ReActConfig[*schema.Message]{Model: m}).WithName("runner_conc")
|
|
runner := NewTypedRunner(RunnerConfig[*schema.Message]{Agent: agent})
|
|
iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("runner conc test")})
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Err != nil {
|
|
errs <- ev.Err
|
|
return
|
|
}
|
|
}
|
|
errs <- nil
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Errorf("concurrent runner failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Tool-related concurrency ----
|
|
|
|
// TestTool_ConcurrentAgent verifies AgentTool with concurrent parent agents.
|
|
func TestTool_ConcurrentAgent(t *testing.T) {
|
|
innerM := &mockModel{}
|
|
innerM.addResp("inner agent result")
|
|
innerAgent := NewReActAgent(&ReActConfig[*schema.Message]{Model: innerM}).WithName("inner_conc").WithDescription("inner agent")
|
|
|
|
ctx := context.Background()
|
|
agentTool := NewAgentTool(ctx, innerAgent)
|
|
|
|
parentM := &forcedToolModel{
|
|
inner: &mockModel{},
|
|
toolCalls: []schema.ToolCall{{ID: "conc_tc", Function: schema.ToolCallFunction{Name: "inner_conc", Arguments: "{}"}}},
|
|
finalResp: "parent done",
|
|
firstCall: true,
|
|
}
|
|
parent := NewReActAgent(&ReActConfig[*schema.Message]{
|
|
Model: parentM, Tools: []Tool{agentTool},
|
|
ToolsConfig: &ToolsNodeConfig{Tools: []Tool{agentTool}},
|
|
}).WithName("parent_conc")
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 5)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
runner := NewTypedRunner(RunnerConfig[*schema.Message]{Agent: parent})
|
|
iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("use tool conc")})
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Err != nil {
|
|
errs <- ev.Err
|
|
return
|
|
}
|
|
}
|
|
errs <- nil
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Errorf("concurrent agent tool failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Sequential workflow concurrency ----
|
|
|
|
// TestWorkflow_SequentialConcurrent verifies multiple sequential workflows
|
|
// run concurrently without interference.
|
|
func TestWorkflow_SequentialConcurrent(t *testing.T) {
|
|
ctx := context.Background()
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 6)
|
|
|
|
for i := 0; i < 6; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
m1 := &mockModel{}
|
|
m1.addResp("seq_a_conc")
|
|
m1.addResp("seq_a_conc")
|
|
m2 := &mockModel{}
|
|
m2.addResp("seq_b_conc")
|
|
m2.addResp("seq_b_conc")
|
|
a1 := NewReActAgent(&ReActConfig[*schema.Message]{Model: m1}).WithName("seq_conc_a")
|
|
a2 := NewReActAgent(&ReActConfig[*schema.Message]{Model: m2}).WithName("seq_conc_b")
|
|
|
|
seq, err := NewSequential(ctx, &SequentialConfig{
|
|
Name: "seq_conc",
|
|
SubAgents: []Agent{a1, a2},
|
|
})
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
runner := NewTypedRunner(RunnerConfig[*schema.Message]{Agent: seq})
|
|
iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("seq conc test")})
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Err != nil {
|
|
errs <- ev.Err
|
|
return
|
|
}
|
|
}
|
|
errs <- nil
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Errorf("sequential workflow conc error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Cancel concurrency ----
|
|
|
|
// TestCancel_ConcurrentTrigger verifies cancel can be triggered concurrently.
|
|
func TestCancel_ConcurrentTrigger(t *testing.T) {
|
|
m := newCancelTestChatModel(nil)
|
|
m.addResp("will be cancelled")
|
|
m.setDelay(100 * time.Millisecond)
|
|
|
|
agent := NewReActAgent(&ReActConfig[*schema.Message]{Model: m}).WithName("cancel_conc")
|
|
|
|
cancelOpt, cancelFunc := WithCancel()
|
|
store := newCancelTestStore()
|
|
runner := NewTypedRunner(RunnerConfig[*schema.Message]{Agent: agent, CheckPointStore: store})
|
|
ctx := context.Background()
|
|
iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("cancel conc test")}, cancelOpt)
|
|
|
|
// Trigger cancel from multiple goroutines
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 3; i++ {
|
|
wg.Go(func() {
|
|
cancelFunc(WithCancelMode(CancelImmediate))
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
// Drain
|
|
time.Sleep(20 * time.Millisecond)
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Err != nil {
|
|
t.Logf("cancel err: %v", ev.Err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Interrupt concurrency ----
|
|
|
|
// TestInterrupt_Concurrent verifies interrupt state can be read concurrently.
|
|
func TestInterrupt_Concurrent(t *testing.T) {
|
|
ctx := context.Background()
|
|
model := &forcedToolModel{
|
|
inner: &mockModel{},
|
|
toolCalls: []schema.ToolCall{{ID: "ci", Function: schema.ToolCallFunction{Name: "ci_tool", Arguments: "{}"}}},
|
|
finalResp: "ci done",
|
|
firstCall: true,
|
|
}
|
|
tool := &mockTool{name: "ci_tool", desc: "concurrent interrupt tool"}
|
|
agent := NewReActAgent(&ReActConfig[*schema.Message]{
|
|
Model: model, Tools: []Tool{tool},
|
|
ToolsConfig: &ToolsNodeConfig{Tools: []Tool{tool}},
|
|
}).WithName("ci_agent")
|
|
|
|
store := newCancelTestStore()
|
|
runner := NewTypedRunner(RunnerConfig[*schema.Message]{Agent: agent, CheckPointStore: store})
|
|
iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("ci test")})
|
|
|
|
// Single consumer — iter.Next() is not safe for concurrent access.
|
|
for {
|
|
_, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
}
|