Documentation
¶
Overview ¶
Package nodes_supervisor provides finite child-pipeline fan-out through supervisor.run and long-lived service orchestration through supervisor.serve.
Finite fan-out:
{ tasks: [
{ id: "a", dsl: "noop", payload: { x: 1 } },
{ id: "b", dsl: "log.info", timeout_ms: 500 }
]} -> supervisor.run
Long-lived services are declared directly in NFlow and signal readiness through compiler/contracts when transport-specific startup completes.
Index ¶
Constants ¶
const ID = "nodes_supervisor"
Variables ¶
var Serve = action.New("supervisor.serve", func(ctx context.Context, req ServeReq) (any, error) { if ctx == nil { return nil, xerr.BadRequest("supervisor.serve: context is nil") } nodes := make([]serviceNode, len(req.Services)) for i, service := range req.Services { enabled := service.Enabled == nil || *service.Enabled nodes[i] = serviceNode{ id: service.ID, action: service.Action, dependsOn: service.DependsOn, enabled: enabled, payload: service.Payload, } } plan, err := buildServicePlan(nodes) if err != nil { return nil, xerr.BadRequest("supervisor.serve: " + err.Error()) } resolver := contracts.ActionResolverFromContext(ctx) if resolver == nil { return nil, xerr.Internal("supervisor.serve: no action resolver in context") } actions := make([]action.AnyAction, len(plan)) enabledCount := 0 for i, service := range plan { act, found := resolver.Action(service.action) if !found || act == nil { return nil, xerr.NotFound("supervisor.serve: action " + service.action + " not found") } actions[i] = act if service.enabled { enabledCount++ } } if enabledCount == 0 { return nil, xerr.BadRequest("supervisor.serve: no enabled services") } if err := ctx.Err(); err != nil { return nil, nil } ready := make([]*contracts.ReadySignal, len(plan)) for i := range ready { ready[i] = contracts.NewReadySignal() } group, groupCtx := errgroup.WithContext(ctx) for i := range plan { if !plan[i].enabled { continue } current, act, signal := plan[i], actions[i], ready[i] group.Go(func() (serviceErr error) { defer func() { signal.MarkStopped() if recovered := recover(); recovered != nil { serviceErr = fmt.Errorf("service %q panicked: %v", current.id, recovered) } }() for _, dependency := range current.dependencies { select { case <-ready[dependency].Done(): case <-groupCtx.Done(): return nil } } if groupCtx.Err() != nil { return nil } serviceCtx := contracts.WithServiceReady(groupCtx, signal) _, invokeErr := action.InvokeAny(serviceCtx, act, current.payload) if ctx.Err() != nil { return nil } if invokeErr == nil { return fmt.Errorf("service %q exited unexpectedly without an error", current.id) } return fmt.Errorf("service %q exited unexpectedly: %w", current.id, invokeErr) }) } return nil, group.Wait() }). Description("Run long-lived services with readiness dependencies and sibling cancellation"). Tag("supervisor", "serve"). Build()
Serve starts long-lived actions, waits for their declared dependencies to become ready, and cancels siblings when any service exits unexpectedly. Parent-context cancellation is a clean shutdown. Services must honor their context; Go cannot forcibly stop a non-cooperative action.
var Supervisor = action.New("supervisor.run", func(ctx context.Context, req SupervisorReq) (SupervisorRes, error) { if len(req.Tasks) == 0 { return SupervisorRes{}, xerr.BadRequest("supervisor: no child tasks provided") } compiler := contracts.CompilerFromContext(ctx) if compiler == nil { return SupervisorRes{}, xerr.Internal("supervisor: no pipeline compiler in context") } results := make([]ChildResult, len(req.Tasks)) group, groupCtx := errgroup.WithContext(ctx) for i, task := range req.Tasks { index, current := i, task group.Go(func() error { results[index] = runChild(groupCtx, compiler, current) return nil }) } if err := group.Wait(); err != nil { return SupervisorRes{}, err } var succeeded, failed int for i := range results { r := &results[i] if r.Error != "" { failed++ } else { succeeded++ } } return SupervisorRes{ Total: len(results), Succeeded: succeeded, Failed: failed, Results: results, }, nil }).Description("Compile and run named child pipelines concurrently"). Build()
Supervisor compiles and runs each task's DSL. All children run concurrently; each child is isolated (panic, error, timeout). The compiler is read from the execution context.
Functions ¶
Types ¶
type ChildResult ¶
type ChildResult struct {
TaskID string `json:"task_id"`
Output any `json:"output,omitempty"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"duration_ms"`
}
ChildResult reports one child's outcome. Error is a plain string so the JSON shape stays stable across error kinds.
type ChildTask ¶
type ChildTask struct {
ID string `json:"id"`
DSL string `json:"dsl"`
Payload any `json:"payload"`
TimeoutMS int64 `json:"timeout_ms,omitempty"`
}
ChildTask describes one pipeline to compile and run.
type ServeReq ¶ added in v0.50.0
type ServeReq struct {
Services []ServiceSpec `json:"services"`
}
ServeReq is the static service graph accepted by supervisor.serve.
type ServiceSpec ¶ added in v0.50.0
type ServiceSpec struct {
ID string `json:"id"`
Action string `json:"action"`
DependsOn []string `json:"depends_on,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Payload any `json:"payload,omitempty"`
}
ServiceSpec describes one long-lived action in a supervisor. An omitted Enabled field means true; a false value excludes the service from startup.
type SupervisorReq ¶
type SupervisorReq struct {
Tasks []ChildTask `json:"tasks" validate:"required"`
}
SupervisorReq is the set of children to run.
type SupervisorRes ¶
type SupervisorRes struct {
Total int `json:"total"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Results []ChildResult `json:"results"`
}
SupervisorRes aggregates the outcome of every child.