Files
ragflow/internal/harness/graph/pregel/remote.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

285 lines
7.7 KiB
Go

// Package pregel provides remote execution support for Pregel.
package pregel
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"ragflow/internal/harness/graph/types"
)
// RemoteRunnable executes nodes remotely via HTTP.
type RemoteRunnable struct {
url string
headers map[string]string
httpClient *http.Client
timeout time.Duration
}
// RemoteConfig configures a remote runnable.
type RemoteConfig struct {
URL string
Headers map[string]string
Timeout time.Duration
HTTPClient *http.Client
}
// NewRemoteRunnable creates a new remote runnable.
func NewRemoteRunnable(config *RemoteConfig) *RemoteRunnable {
timeout := config.Timeout
if timeout == 0 {
timeout = 30 * time.Second
}
client := config.HTTPClient
if client == nil {
client = &http.Client{
Timeout: timeout,
}
}
headers := make(map[string]string)
if config.Headers != nil {
for k, v := range config.Headers {
headers[k] = v
}
}
return &RemoteRunnable{
url: config.URL,
headers: headers,
httpClient: client,
timeout: timeout,
}
}
// Execute sends a request to the remote server to execute a node.
func (r *RemoteRunnable) Execute(ctx context.Context, nodeName string, input any, config *types.RunnableConfig) (any, error) {
// Build request
reqBody := &RemoteExecuteRequest{
Node: nodeName,
Input: input,
Config: config,
}
data, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", r.url+"/execute", bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
for k, v := range r.headers {
req.Header.Set(k, v)
}
// Send request
resp, err := r.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("remote execution failed with status %d: %s", resp.StatusCode, string(body))
}
// Parse response
var result RemoteExecuteResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
if result.Error != "" {
return nil, fmt.Errorf("remote execution error: %s", result.Error)
}
return result.Output, nil
}
// RemoteExecuteRequest represents a request to execute a node remotely.
type RemoteExecuteRequest struct {
Node string `json:"node"`
Input any `json:"input"`
Config *types.RunnableConfig `json:"config,omitempty"`
}
// RemoteExecuteResponse represents the response from a remote execution.
type RemoteExecuteResponse struct {
Output any `json:"output,omitempty"`
Error string `json:"error,omitempty"`
}
// PregelProtocol defines the protocol for remote Pregel execution.
type PregelProtocol interface {
// Send sends a message to the remote peer.
Send(ctx context.Context, message *PregelMessage) error
// Receive receives a message from the remote peer.
Receive(ctx context.Context) (*PregelMessage, error)
// Close closes the protocol connection.
Close() error
}
// PregelMessage represents a message in the Pregel protocol.
type PregelMessage struct {
Type MessageType `json:"type"`
ID string `json:"id"`
NodeName string `json:"node_name,omitempty"`
Input any `json:"input,omitempty"`
Output any `json:"output,omitempty"`
Error string `json:"error,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// MessageType represents the type of a Pregel message.
type MessageType string
const (
// MessageTypeExecute requests node execution.
MessageTypeExecute MessageType = "execute"
// MessageTypeExecuteResponse is the response to an execute request.
MessageTypeExecuteResponse MessageType = "execute_response"
// MessageTypeCheckpoint sends checkpoint data.
MessageTypeCheckpoint MessageType = "checkpoint"
// MessageTypeStateUpdate sends state updates.
MessageTypeStateUpdate MessageType = "state_update"
// MessageTypeInterrupt sends interrupt information.
MessageTypeInterrupt MessageType = "interrupt"
// MessageTypeResume resumes execution from interrupt.
MessageTypeResume MessageType = "resume"
// MessageTypePing is a heartbeat.
MessageTypePing MessageType = "ping"
// MessageTypePong is a heartbeat response.
MessageTypePong MessageType = "pong"
)
// HTTPPregelProtocol implements PregelProtocol over HTTP.
type HTTPPregelProtocol struct {
baseURL string
httpClient *http.Client
headers map[string]string
}
// NewHTTPPregelProtocol creates a new HTTP Pregel protocol.
func NewHTTPPregelProtocol(baseURL string, headers map[string]string) *HTTPPregelProtocol {
return &HTTPPregelProtocol{
baseURL: baseURL,
httpClient: &http.Client{Timeout: 30 * time.Second},
headers: headers,
}
}
// Send sends a message to the remote peer.
func (p *HTTPPregelProtocol) Send(ctx context.Context, message *PregelMessage) error {
data, err := json.Marshal(message)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", p.baseURL+"/pregel/message", bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
for k, v := range p.headers {
req.Header.Set(k, v)
}
resp, err := p.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("send failed with status %d: %s", resp.StatusCode, string(body))
}
return nil
}
// Receive receives a message from the remote peer.
func (p *HTTPPregelProtocol) Receive(ctx context.Context) (*PregelMessage, error) {
req, err := http.NewRequestWithContext(ctx, "GET", p.baseURL+"/pregel/message", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
for k, v := range p.headers {
req.Header.Set(k, v)
}
resp, err := p.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to receive message: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return nil, nil // No message available
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("receive failed with status %d: %s", resp.StatusCode, string(body))
}
var message PregelMessage
if err := json.NewDecoder(resp.Body).Decode(&message); err != nil {
return nil, fmt.Errorf("failed to decode message: %w", err)
}
return &message, nil
}
// Close closes the protocol connection.
func (p *HTTPPregelProtocol) Close() error {
return nil
}
// RemoteNode wraps a remote execution capability as a node function.
type RemoteNode struct {
runnable *RemoteRunnable
nodeName string
}
// NewRemoteNode creates a new remote node.
func NewRemoteNode(runnable *RemoteRunnable, nodeName string) *RemoteNode {
return &RemoteNode{
runnable: runnable,
nodeName: nodeName,
}
}
// Execute executes the remote node.
func (n *RemoteNode) Execute(ctx context.Context, input any) (any, error) {
return n.runnable.Execute(ctx, n.nodeName, input, nil)
}
// NodeToRemoteRunnable converts a local node to a remote runnable.
func NodeToRemoteRunnable(node types.NodeFunc, url string) *RemoteRunnable {
// This would register the node locally and expose it via HTTP
// Implementation depends on the HTTP server setup
return NewRemoteRunnable(&RemoteConfig{
URL: url,
})
}