mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
114 lines
2.9 KiB
Go
114 lines
2.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/lyzrai/flow/pkg/models"
|
|
)
|
|
|
|
// retryableNodeTypes lists node types where retry makes sense (I/O, external calls).
|
|
// Deterministic nodes (If, Set, Merge, etc.) never retry.
|
|
var retryableNodeTypes = map[string]bool{
|
|
"flow-nodes-base.httpRequest": true,
|
|
"flow-nodes-base.code": true,
|
|
"flow-nodes-base.executeWorkflow": true,
|
|
}
|
|
|
|
// executeWithRetry wraps an executor call with optional retry logic from node.Settings.
|
|
// Only applies to retryable node types. Deterministic nodes skip retry.
|
|
func executeWithRetry(ctx context.Context, executorFn NodeExecutorFunc, node models.NodeDef, inputs [][]models.Item, execCtx *ExecutionContext) (map[int][]models.Item, error) {
|
|
retryOnFail, _ := node.Settings["retryOnFail"].(bool)
|
|
if !retryOnFail || !retryableNodeTypes[node.Type] {
|
|
return executorFn(ctx, node, inputs, execCtx)
|
|
}
|
|
|
|
maxTries := 3
|
|
if v, ok := node.Settings["maxTries"].(float64); ok && v > 0 {
|
|
maxTries = int(v)
|
|
}
|
|
waitMs := 1000
|
|
if v, ok := node.Settings["waitBetweenTries"].(float64); ok && v > 0 {
|
|
waitMs = int(v)
|
|
}
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt <= maxTries; attempt++ {
|
|
outputs, err := executorFn(ctx, node, inputs, execCtx)
|
|
if err == nil {
|
|
if attempt > 0 {
|
|
slog.InfoContext(ctx, "node_retry_succeeded",
|
|
slog.String("node", node.Name),
|
|
slog.Int("attempt", attempt+1),
|
|
)
|
|
}
|
|
return outputs, nil
|
|
}
|
|
lastErr = err
|
|
if attempt < maxTries {
|
|
slog.WarnContext(ctx, "node_retry",
|
|
slog.String("node", node.Name),
|
|
slog.Int("attempt", attempt+1),
|
|
slog.Int("max", maxTries+1),
|
|
slog.Any("error", err),
|
|
)
|
|
time.Sleep(time.Duration(waitMs) * time.Millisecond)
|
|
}
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
// allInputsEmpty returns true if all input slots are empty.
|
|
func allInputsEmpty(inputs [][]models.Item) bool {
|
|
for _, group := range inputs {
|
|
if len(group) > 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// forwardInputs stores the node's inputs as its outputs so downstream nodes
|
|
// aren't starved when an executor fails or is missing.
|
|
func forwardInputs(ctx *ExecutionContext, nodeName string, inputs [][]models.Item) {
|
|
var all []models.Item
|
|
for _, input := range inputs {
|
|
all = append(all, input...)
|
|
}
|
|
if len(all) > 0 {
|
|
ctx.SetOutput(nodeName, 0, all)
|
|
}
|
|
}
|
|
|
|
func getTerminalOutputs(ctx *ExecutionContext, dag *DAG) map[string]map[int][]models.Item {
|
|
result := make(map[string]map[int][]models.Item)
|
|
allOutputs := ctx.AllOutputs()
|
|
|
|
for nodeName := range dag.Nodes {
|
|
if ctx.IsDelegated(nodeName) {
|
|
continue
|
|
}
|
|
if isEffectiveTerminal(ctx, dag, nodeName) {
|
|
if out, ok := allOutputs[nodeName]; ok {
|
|
result[nodeName] = out
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func isEffectiveTerminal(ctx *ExecutionContext, dag *DAG, nodeName string) bool {
|
|
edges := dag.Adjacency[nodeName]
|
|
if len(edges) == 0 {
|
|
return true
|
|
}
|
|
for _, edge := range edges {
|
|
if !ctx.IsDelegated(edge.Target) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|