Files
ragflow/internal/harness/graph/pregel/async.go
Yingfeng 706e0d2d06 Refactor harness framework (#16271)
### What problem does this PR solve?

- Tools management
- Pregel engine wrapper for better usage
- UT race
- Coding style

### Type of change

- [x] Refactoring
2026-06-23 20:18:04 +08:00

495 lines
10 KiB
Go

// Package pregel provides async execution pipeline for Pregel.
package pregel
import (
"context"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"ragflow/internal/harness/graph/types"
)
// AsyncExecutor provides async execution of nodes with concurrency control.
type AsyncExecutor struct {
maxConcurrency int
workerPool chan struct{}
results chan *asyncTaskResult
mu sync.Mutex
activeTasks map[string]*asyncTask
}
// asyncTask represents an asynchronous task.
type asyncTask struct {
ID string
Name string
Func func(context.Context) (any, error)
Context context.Context
Cancel context.CancelFunc
Priority int
}
// asyncTaskResult represents the result of an async task.
type asyncTaskResult struct {
TaskID string
Name string
Output any
Err error
Duration time.Duration
}
// NewAsyncExecutor creates a new async executor.
func NewAsyncExecutor(maxConcurrency int) *AsyncExecutor {
if maxConcurrency <= 0 {
maxConcurrency = 10 // Default concurrency
}
exec := &AsyncExecutor{
maxConcurrency: maxConcurrency,
workerPool: make(chan struct{}, maxConcurrency),
results: make(chan *asyncTaskResult, 100),
activeTasks: make(map[string]*asyncTask),
}
// Pre-fill worker pool
for i := 0; i < maxConcurrency; i++ {
exec.workerPool <- struct{}{}
}
return exec
}
// Execute executes a single task asynchronously.
func (e *AsyncExecutor) Execute(ctx context.Context, name string, fn func(context.Context) (any, error)) <-chan *asyncTaskResult {
resultCh := make(chan *asyncTaskResult, 1)
// Create cancellable context so Cancel() can stop running tasks.
taskCtx, cancel := context.WithCancel(ctx)
task := &asyncTask{
ID: uuid.New().String(),
Name: name,
Func: fn,
Context: taskCtx,
Cancel: cancel,
}
e.mu.Lock()
e.activeTasks[task.ID] = task
e.mu.Unlock()
go func() {
defer close(resultCh)
// Remove from activeTasks on ALL exit paths (success, ctx cancelled, etc.).
defer func() {
e.mu.Lock()
delete(e.activeTasks, task.ID)
e.mu.Unlock()
}()
startTime := time.Now()
// Acquire worker slot
select {
case <-e.workerPool:
defer func() { e.workerPool <- struct{}{} }()
case <-ctx.Done():
resultCh <- &asyncTaskResult{
TaskID: task.ID,
Name: task.Name,
Err: ctx.Err(),
}
return
}
// Execute task
output, err := task.Func(task.Context)
result := &asyncTaskResult{
TaskID: task.ID,
Name: task.Name,
Output: output,
Err: err,
Duration: time.Since(startTime),
}
resultCh <- result
}()
return resultCh
}
// ExecuteBatch executes multiple tasks concurrently with controlled concurrency.
func (e *AsyncExecutor) ExecuteBatch(ctx context.Context, tasks []asyncTask) <-chan *asyncTaskResult {
resultCh := make(chan *asyncTaskResult, len(tasks))
// Register all tasks in activeTasks before any goroutine starts.
for i := range tasks {
tasks[i].ID = uuid.New().String()
e.mu.Lock()
e.activeTasks[tasks[i].ID] = &tasks[i]
e.mu.Unlock()
}
var wg sync.WaitGroup
for i := range tasks {
wg.Add(1)
go func(task *asyncTask) {
defer wg.Done()
defer func() {
e.mu.Lock()
delete(e.activeTasks, task.ID)
e.mu.Unlock()
}()
startTime := time.Now()
// Acquire worker slot
select {
case <-e.workerPool:
defer func() { e.workerPool <- struct{}{} }()
case <-ctx.Done():
resultCh <- &asyncTaskResult{
TaskID: task.ID,
Name: task.Name,
Err: ctx.Err(),
}
return
}
// Execute task
output, err := task.Func(task.Context)
resultCh <- &asyncTaskResult{
TaskID: task.ID,
Name: task.Name,
Output: output,
Err: err,
Duration: time.Since(startTime),
}
}(&tasks[i])
}
go func() {
wg.Wait()
close(resultCh)
}()
return resultCh
}
// ExecuteWithRetry executes a task with retry logic.
func (e *AsyncExecutor) ExecuteWithRetry(ctx context.Context, name string, fn func(context.Context) (any, error), retryConfig *RetryConfig) <-chan *asyncTaskResult {
resultCh := make(chan *asyncTaskResult, 1)
taskCtx, cancel := context.WithCancel(ctx)
task := &asyncTask{
ID: uuid.New().String(),
Name: name,
Context: taskCtx,
Cancel: cancel,
}
e.mu.Lock()
e.activeTasks[task.ID] = task
e.mu.Unlock()
go func() {
defer close(resultCh)
executor := NewRetryExecutor(retryConfig.Policy)
startTime := time.Now()
output, err := executor.Execute(task.Context, name, fn)
result := &asyncTaskResult{
TaskID: task.ID,
Name: name,
Output: output,
Err: err,
Duration: time.Since(startTime),
}
e.mu.Lock()
delete(e.activeTasks, task.ID)
e.mu.Unlock()
resultCh <- result
}()
return resultCh
}
// Cancel cancels all active tasks by invoking their cancel functions.
func (e *AsyncExecutor) Cancel() {
e.mu.Lock()
defer e.mu.Unlock()
for id, task := range e.activeTasks {
if task.Cancel != nil {
task.Cancel()
}
delete(e.activeTasks, id)
}
}
// GetActiveTaskCount returns the number of currently active tasks.
func (e *AsyncExecutor) GetActiveTaskCount() int {
e.mu.Lock()
defer e.mu.Unlock()
return len(e.activeTasks)
}
// Wait waits for all active tasks to complete.
func (e *AsyncExecutor) Wait(ctx context.Context) error {
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if e.GetActiveTaskCount() == 0 {
return nil
}
}
}
}
// AsyncPipeline provides an async execution pipeline for the Pregel loop.
type AsyncPipeline struct {
executor *AsyncExecutor
retryer *RetryExecutor
// Stream channels
events chan any
errors chan error
// Control
mu sync.RWMutex
cancel context.CancelFunc
running bool
}
// NewAsyncPipeline creates a new async pipeline.
func NewAsyncPipeline(maxConcurrency int, retryPolicy *types.RetryPolicy) *AsyncPipeline {
return &AsyncPipeline{
executor: NewAsyncExecutor(maxConcurrency),
retryer: NewRetryExecutor(retryPolicy),
events: make(chan any, 100),
errors: make(chan error, 10),
running: false,
}
}
// Start starts the async pipeline.
func (p *AsyncPipeline) Start(ctx context.Context) context.Context {
p.mu.Lock()
defer p.mu.Unlock()
if p.running {
return ctx
}
// Reinitialize channels that were closed by Stop().
p.events = make(chan any, 100)
p.errors = make(chan error, 10)
ctx, p.cancel = context.WithCancel(ctx)
p.running = true
return ctx
}
// Stop stops the async pipeline.
func (p *AsyncPipeline) Stop() {
p.mu.Lock()
defer p.mu.Unlock()
if !p.running {
return
}
if p.cancel != nil {
p.cancel()
}
p.executor.Cancel()
close(p.events)
close(p.errors)
p.running = false
}
// ExecuteNode executes a node in the pipeline.
func (p *AsyncPipeline) ExecuteNode(ctx context.Context, name string, fn func(context.Context) (any, error), retryConfig *RetryConfig) <-chan *asyncTaskResult {
if retryConfig != nil {
return p.executor.ExecuteWithRetry(ctx, name, fn, retryConfig)
}
return p.executor.Execute(ctx, name, fn)
}
// Events returns the event channel.
func (p *AsyncPipeline) Events() <-chan any {
return p.events
}
// Errors returns the error channel.
func (p *AsyncPipeline) Errors() <-chan error {
return p.errors
}
// EmitEvent emits an event to the pipeline.
func (p *AsyncPipeline) EmitEvent(event any) {
p.mu.RLock()
defer p.mu.RUnlock()
if p.running {
select {
case p.events <- event:
default:
// Channel full, drop event
}
}
}
// EmitError emits an error to the pipeline.
func (p *AsyncPipeline) EmitError(err error) {
p.mu.RLock()
defer p.mu.RUnlock()
if p.running {
select {
case p.errors <- err:
default:
// Channel full, drop error
}
}
}
// IsRunning returns whether the pipeline is running.
func (p *AsyncPipeline) IsRunning() bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.running
}
// ConcurrencyLimiter limits concurrency for specific nodes.
type ConcurrencyLimiter struct {
limits map[string]chan struct{}
mu sync.RWMutex
}
// NewConcurrencyLimiter creates a new concurrency limiter.
func NewConcurrencyLimiter() *ConcurrencyLimiter {
return &ConcurrencyLimiter{
limits: make(map[string]chan struct{}),
}
}
// SetLimit sets the concurrency limit for a node.
func (l *ConcurrencyLimiter) SetLimit(node string, limit int) {
l.mu.Lock()
defer l.mu.Unlock()
ch := make(chan struct{}, limit)
for i := 0; i < limit; i++ {
ch <- struct{}{}
}
l.limits[node] = ch
}
// Acquire acquires a slot for a node.
func (l *ConcurrencyLimiter) Acquire(ctx context.Context, node string) error {
l.mu.RLock()
ch, ok := l.limits[node]
l.mu.RUnlock()
if !ok {
// No limit set
return nil
}
select {
case <-ch:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// Release releases a slot for a node.
func (l *ConcurrencyLimiter) Release(node string) {
l.mu.RLock()
ch, ok := l.limits[node]
l.mu.RUnlock()
if !ok {
return
}
select {
case ch <- struct{}{}:
default:
// Channel full, shouldn't happen
}
}
// PriorityTask represents a prioritized task.
type PriorityTask struct {
Func func(context.Context) (any, error)
Priority int
}
// PriorityExecutor executes tasks with priority scheduling.
type PriorityExecutor struct {
tasks chan PriorityTask
mu sync.Mutex
}
// NewPriorityExecutor creates a new priority executor.
func NewPriorityExecutor(bufferSize int) *PriorityExecutor {
return &PriorityExecutor{
tasks: make(chan PriorityTask, bufferSize),
}
}
// Submit submits a task with priority.
func (e *PriorityExecutor) Submit(task PriorityTask) error {
select {
case e.tasks <- task:
return nil
default:
return fmt.Errorf("task queue full")
}
}
// Execute executes tasks in priority order.
func (e *PriorityExecutor) Execute(ctx context.Context, maxConcurrency int) <-chan any {
resultCh := make(chan any, maxConcurrency)
// Simple priority scheduling using multiple channels
// In a real implementation, you'd use a priority queue
go func() {
defer close(resultCh)
// This is a simplified implementation
// A full implementation would use a heap-based priority queue
for {
select {
case <-ctx.Done():
return
case task := <-e.tasks:
output, err := task.Func(ctx)
resultCh <- map[string]any{
"output": output,
"error": err,
"priority": task.Priority,
}
}
}
}()
return resultCh
}