mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-06 11:28:38 +08:00
### What problem does this PR solve? - Tools management - Pregel engine wrapper for better usage - UT race - Coding style ### Type of change - [x] Refactoring
426 lines
10 KiB
Go
426 lines
10 KiB
Go
// Package pregel provides Pregel algorithm optimizations for graph execution.
|
|
package pregel
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"ragflow/internal/harness/graph/channels"
|
|
"ragflow/internal/harness/graph/types"
|
|
)
|
|
|
|
func TestPriorityTaskQueue(t *testing.T) {
|
|
pq := NewPriorityTaskQueue()
|
|
|
|
// Test push and pop
|
|
task1 := &Task{Name: "task1", Path: []string{"a"}}
|
|
task2 := &Task{Name: "task2", Path: []string{"b"}}
|
|
task3 := &Task{Name: "task3", Path: []string{"a", "b"}}
|
|
|
|
pq.Push(task1)
|
|
pq.Push(task2)
|
|
pq.Push(task3)
|
|
|
|
if pq.Len() != 3 {
|
|
t.Errorf("Expected 3 tasks, got %d", pq.Len())
|
|
}
|
|
|
|
// Test pop - should return shortest path first
|
|
popped := pq.Pop()
|
|
if popped == nil {
|
|
t.Fatal("Expected non-nil popped task")
|
|
}
|
|
|
|
// task1 has path ["a"] which is shorter than task3's ["a","b"]
|
|
if popped.Name != "task1" && popped.Name != "task2" {
|
|
t.Errorf("Expected task1 or task2, got %s", popped.Name)
|
|
}
|
|
|
|
// Check queue size
|
|
if pq.Len() != 2 {
|
|
t.Errorf("Expected 2 tasks remaining, got %d", pq.Len())
|
|
}
|
|
}
|
|
|
|
func TestPriorityTaskQueueOrdering(t *testing.T) {
|
|
pq := NewPriorityTaskQueue()
|
|
|
|
// Add tasks with different path lengths
|
|
tasks := []*Task{
|
|
{Name: "deep1", Path: []string{"a", "b", "c"}},
|
|
{Name: "shallow1", Path: []string{"a"}},
|
|
{Name: "deep2", Path: []string{"a", "b"}},
|
|
{Name: "shallow2", Path: []string{"b"}},
|
|
}
|
|
|
|
for _, task := range tasks {
|
|
pq.Push(task)
|
|
}
|
|
|
|
// Pop in priority order
|
|
// Shallow paths should come first
|
|
order := make([]string, 0, 4)
|
|
for i := 0; i < 4; i++ {
|
|
task := pq.Pop()
|
|
if task != nil {
|
|
order = append(order, task.Name)
|
|
}
|
|
}
|
|
|
|
// Verify shallow tasks come first
|
|
if order[0] != "shallow1" && order[0] != "shallow2" {
|
|
t.Errorf("Expected shallow task first, got %s", order[0])
|
|
}
|
|
}
|
|
|
|
func TestTaskPriorityComparison(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tp1 *TaskPriority
|
|
tp2 *TaskPriority
|
|
expected int
|
|
}{
|
|
{
|
|
name: "different priorities",
|
|
tp1: NewTaskPriority("t1", []string{"a"}, 2),
|
|
tp2: NewTaskPriority("t2", []string{"b"}, 1),
|
|
expected: 1, // t1 has higher priority (lower number)
|
|
},
|
|
{
|
|
name: "different path lengths",
|
|
tp1: NewTaskPriority("t1", []string{"a"}, 0),
|
|
tp2: NewTaskPriority("t2", []string{"a", "b"}, 0),
|
|
expected: -1, // t1 has shorter path
|
|
},
|
|
{
|
|
name: "same path length, lexicographic",
|
|
tp1: NewTaskPriority("t1", []string{"a"}, 0),
|
|
tp2: NewTaskPriority("t2", []string{"b"}, 0),
|
|
expected: -1, // t1 comes first alphabetically
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.tp1.Compare(tt.tp2)
|
|
if result != tt.expected {
|
|
t.Errorf("Compare() = %d, expected %d", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPregelOptimizedEngineCreation(t *testing.T) {
|
|
baseEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
|
|
optimized := NewPregelOptimizedEngine(baseEngine, nil)
|
|
|
|
if optimized == nil {
|
|
t.Fatal("Expected non-nil optimized engine")
|
|
}
|
|
|
|
if optimized.Engine != baseEngine {
|
|
t.Error("Expected base engine to match")
|
|
}
|
|
|
|
if !optimized.config.BumpStep {
|
|
t.Error("Expected bump_step to be enabled")
|
|
}
|
|
|
|
if !optimized.config.FinishNotification {
|
|
t.Error("Expected finish notification to be enabled")
|
|
}
|
|
|
|
if optimized.taskPriorityQueue == nil {
|
|
t.Error("Expected task queue to be initialized")
|
|
}
|
|
}
|
|
|
|
func TestFinishNotification(t *testing.T) {
|
|
t.Run("send finish notification", func(t *testing.T) {
|
|
// This would test actual notification sending
|
|
// For now, just verify the structure
|
|
notification := &FinishNotification{
|
|
TaskName: "test_node",
|
|
Output: map[string]any{"result": "success"},
|
|
Step: 1,
|
|
Namespace: "test_namespace",
|
|
}
|
|
|
|
if notification.TaskName != "test_node" {
|
|
t.Errorf("Expected task name 'test_node', got %s", notification.TaskName)
|
|
}
|
|
|
|
if notification.Step != 1 {
|
|
t.Errorf("Expected step 1, got %d", notification.Step)
|
|
}
|
|
|
|
if notification.Namespace != "test_namespace" {
|
|
t.Errorf("Expected namespace 'test_namespace', got %s", notification.Namespace)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBumpStep(t *testing.T) {
|
|
baseEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
|
|
optimized := NewPregelOptimizedEngine(baseEngine, nil)
|
|
|
|
t.Run("bump step for dependent tasks", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Simulate task completion
|
|
updatedChannels := map[string]struct{}{
|
|
"channel1": {},
|
|
"channel2": {},
|
|
}
|
|
|
|
err := optimized.BumpStep(ctx, "task1", 1, updatedChannels)
|
|
if err != nil {
|
|
t.Errorf("BumpStep failed: %v", err)
|
|
}
|
|
|
|
// Verify task is marked as finished
|
|
if !optimized.finishedTasks["task1"] {
|
|
t.Error("Expected task1 to be marked as finished")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCompareTaskPriority(t *testing.T) {
|
|
optimized := NewPregelOptimizedEngine(&Engine{}, nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
t1 *Task
|
|
t2 *Task
|
|
expected int
|
|
}{
|
|
{
|
|
name: "different path lengths",
|
|
t1: &Task{Name: "a", Path: []string{"a"}},
|
|
t2: &Task{Name: "b", Path: []string{"a", "b"}},
|
|
expected: -1, // Shorter path first
|
|
},
|
|
{
|
|
name: "same path length, lexicographic",
|
|
t1: &Task{Name: "a", Path: []string{"a"}},
|
|
t2: &Task{Name: "b", Path: []string{"a"}},
|
|
expected: -1, // Alphabetical first
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := optimized.compareTaskPriority(tt.t1, tt.t2)
|
|
if result != tt.expected {
|
|
t.Errorf("compareTaskPriority() = %d, expected %d", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsNodeReady(t *testing.T) {
|
|
optimized := NewPregelOptimizedEngine(&Engine{}, nil)
|
|
|
|
t.Run("node with no dependencies", func(t *testing.T) {
|
|
// Nodes with no required channels are always ready
|
|
ready := optimized.isNodeReady("node1")
|
|
if !ready {
|
|
t.Error("Expected node to be ready with no dependencies")
|
|
}
|
|
})
|
|
|
|
t.Run("node with seen channels", func(t *testing.T) {
|
|
// Mark channels as ready
|
|
optimized.readyChannels["channel1"] = true
|
|
optimized.readyChannels["channel2"] = true
|
|
|
|
// This would need to be set up with proper dependencies
|
|
// For now, just test the mechanism
|
|
_ = optimized.isNodeReady("node1")
|
|
})
|
|
}
|
|
|
|
func TestGetDependencies(t *testing.T) {
|
|
optimized := NewPregelOptimizedEngine(&Engine{}, nil)
|
|
|
|
t.Run("get dependencies", func(t *testing.T) {
|
|
// This would require proper graph setup
|
|
// For now, test that the function exists
|
|
deps := optimized.getDependencies("task1")
|
|
if deps == nil {
|
|
t.Log("getDependencies returns nil (expected for empty graph)")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHasSeenChannel(t *testing.T) {
|
|
optimized := NewPregelOptimizedEngine(&Engine{}, nil)
|
|
|
|
t.Run("channel not seen", func(t *testing.T) {
|
|
seen := optimized.hasSeenChannel("task1", "channel1")
|
|
if seen {
|
|
t.Error("Expected channel to not be seen")
|
|
}
|
|
})
|
|
|
|
t.Run("channel seen", func(t *testing.T) {
|
|
// Initialize seen channels for task
|
|
optimized.seenChannels["task1"] = map[string]bool{
|
|
"channel1": true,
|
|
}
|
|
|
|
seen := optimized.hasSeenChannel("task1", "channel1")
|
|
if !seen {
|
|
t.Error("Expected channel to be seen")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetTriggersForNode(t *testing.T) {
|
|
optimized := NewPregelOptimizedEngine(&Engine{}, nil)
|
|
|
|
t.Run("get triggers", func(t *testing.T) {
|
|
// This would require proper graph setup
|
|
// For now, test that the function exists
|
|
triggers := optimized.getTriggersForNode("node1")
|
|
if triggers == nil {
|
|
t.Log("getTriggersForNode returns nil (expected for empty graph)")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNewTaskPriority(t *testing.T) {
|
|
tp := NewTaskPriority("test_task", []string{"a", "b"}, 1)
|
|
|
|
if tp == nil {
|
|
t.Fatal("Expected non-nil task priority")
|
|
}
|
|
|
|
if tp.Name != "test_task" {
|
|
t.Errorf("Expected name 'test_task', got %s", tp.Name)
|
|
}
|
|
|
|
if len(tp.Path) != 2 {
|
|
t.Errorf("Expected path length 2, got %d", len(tp.Path))
|
|
}
|
|
|
|
if tp.Priority != 1 {
|
|
t.Errorf("Expected priority 1, got %d", tp.Priority)
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueueHeap(t *testing.T) {
|
|
pq := NewPriorityTaskQueue()
|
|
|
|
// Test heap property maintenance
|
|
tasks := []*Task{
|
|
{Name: "z", Path: []string{"z"}},
|
|
{Name: "m", Path: []string{"m"}},
|
|
{Name: "a", Path: []string{"a"}},
|
|
{Name: "n", Path: []string{"n"}},
|
|
}
|
|
|
|
// Add in random order
|
|
for _, task := range tasks {
|
|
pq.Push(task)
|
|
}
|
|
|
|
// Verify heap property: each parent should have higher priority than children
|
|
// This is a simplified check - in practice we'd verify the full heap property
|
|
if pq.Len() != 4 {
|
|
t.Errorf("Expected 4 tasks in queue, got %d", pq.Len())
|
|
}
|
|
}
|
|
|
|
func TestOptimizedApplyWrites(t *testing.T) {
|
|
baseEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
|
|
optimized := NewPregelOptimizedEngine(baseEngine, nil)
|
|
|
|
t.Run("apply writes with bump_step", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a registry with a channel
|
|
registry := channels.NewRegistry()
|
|
ch := channels.NewAnyValue(nil)
|
|
registry.Register("key", ch)
|
|
|
|
results := []*TaskResult{
|
|
{Name: "task1", Output: map[string]any{"key": "value"}},
|
|
}
|
|
|
|
updatedChannels, err := optimized.OptimizedApplyWrites(
|
|
ctx,
|
|
registry,
|
|
results,
|
|
1,
|
|
map[string]struct{}{"trigger": {}},
|
|
)
|
|
|
|
if err != nil {
|
|
t.Logf("OptimizedApplyWrites error: %v", err)
|
|
}
|
|
|
|
if updatedChannels == nil {
|
|
t.Error("Expected non-nil updated channels")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetCurrentNamespace(t *testing.T) {
|
|
baseEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
baseEngine.config.Set("namespace", "test_ns")
|
|
|
|
optimized := NewPregelOptimizedEngine(baseEngine, nil)
|
|
|
|
ns := optimized.getCurrentNamespace()
|
|
|
|
if ns == "" {
|
|
t.Log("getCurrentNamespace returns empty (config may not have namespace)")
|
|
}
|
|
}
|
|
|
|
func TestPrepareNextTasksOptimized(t *testing.T) {
|
|
baseEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
|
|
optimized := NewPregelOptimizedEngine(baseEngine, nil)
|
|
|
|
t.Run("prepare next tasks with optimization", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
tasks, triggers, err := optimized.PrepareNextTasksOptimized(
|
|
ctx,
|
|
nil, // registry
|
|
map[string]bool{},
|
|
"",
|
|
nil, // current state
|
|
)
|
|
|
|
if err != nil {
|
|
t.Logf("PrepareNextTasksOptimized error: %v", err)
|
|
}
|
|
|
|
if tasks == nil {
|
|
tasks = []*Task{}
|
|
}
|
|
|
|
if triggers == nil {
|
|
triggers = map[string]struct{}{}
|
|
}
|
|
|
|
t.Logf("Prepared %d tasks with %d triggers", len(tasks), len(triggers))
|
|
})
|
|
}
|