Help us shape the future of Go! Take the 2026 Go Developer Survey

nodes_supervisor

package
v0.56.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

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

View Source
const ID = "nodes_supervisor"

Variables

View Source
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.

View Source
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

func Bundle

func Bundle(_ map[string]string) compiler.Bundle

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL