dagu/internal/service/frontend/api/v2/dags.go
Yota Hamada f3d4577e42
fix(cmd): refactor start command not to prevent DAG from running unexpectedly (#1497)
* **Behavior Changes**
* Removed explicit no-queue and disable-max-active-runs options;
start/retry flows simplified to default local execution and streamlined
retry semantics.

* **New Features**
* Singleton mode now returns clear HTTP 409 conflicts when a singleton
DAG is already running or queued.
* Added top-level run Error field and an API to record early failures
for quicker failure visibility.

* **Bug Fixes**
* Improved process acquisition and restart/retry error handling; tests
updated to reflect local execution behavior.
2025-12-21 18:42:34 +09:00

951 lines
25 KiB
Go

package api
import (
"context"
"errors"
"fmt"
"net/http"
"runtime"
"sort"
"strings"
"time"
"github.com/dagu-org/dagu/api/v2"
"github.com/dagu-org/dagu/internal/common/config"
"github.com/dagu-org/dagu/internal/core"
"github.com/dagu-org/dagu/internal/core/execution"
"github.com/dagu-org/dagu/internal/core/spec"
runtime1 "github.com/dagu-org/dagu/internal/runtime"
)
// ValidateDAGSpec implements api.StrictServerInterface.
func (a *API) ValidateDAGSpec(ctx context.Context, request api.ValidateDAGSpecRequestObject) (api.ValidateDAGSpecResponseObject, error) {
// Parse and validate the provided spec without persisting it.
// Use AllowBuildErrors so we can return partial DAG details alongside errors.
name := "validated-dag"
if request.Body != nil && request.Body.Name != nil {
name = *request.Body.Name
}
if request.Body == nil {
return nil, &Error{
HTTPStatus: http.StatusBadRequest,
Code: api.ErrorCodeBadRequest,
Message: "request body is required",
}
}
// Load the DAG spec
dag, err := spec.LoadYAML(ctx,
[]byte(request.Body.Spec),
spec.WithName(name),
spec.WithAllowBuildErrors(),
spec.WithoutEval(),
)
var errs []string
var loadErrs core.ErrorList
if errors.As(err, &loadErrs) {
errs = loadErrs.ToStringList()
} else if err != nil {
// Unexpected fatal error
return nil, err
}
if dag != nil && len(dag.BuildErrors) > 0 {
for _, e := range dag.BuildErrors {
errs = append(errs, e.Error())
}
}
// Build response
details := toDAGDetails(dag)
return &api.ValidateDAGSpec200JSONResponse{
Valid: len(errs) == 0,
Dag: details,
Errors: errs,
}, nil
}
func (a *API) CreateNewDAG(ctx context.Context, request api.CreateNewDAGRequestObject) (api.CreateNewDAGResponseObject, error) {
if err := a.isAllowed(config.PermissionWriteDAGs); err != nil {
return nil, err
}
if err := a.requireWrite(ctx); err != nil {
return nil, err
}
// Determine spec to create with: provided spec or default template
var yamlSpec []byte
if request.Body.Spec != nil && strings.TrimSpace(*request.Body.Spec) != "" {
// Validate provided spec before creating
_, err := spec.LoadYAML(ctx,
[]byte(*request.Body.Spec),
spec.WithName(request.Body.Name),
spec.WithoutEval(),
)
if err != nil {
var verrs core.ErrorList
if errors.As(err, &verrs) {
// Return 400 with summary of errors
return nil, &Error{
HTTPStatus: http.StatusBadRequest,
Code: api.ErrorCodeBadRequest,
Message: strings.Join(verrs.ToStringList(), "; "),
}
}
return nil, &Error{
HTTPStatus: http.StatusBadRequest,
Code: api.ErrorCodeBadRequest,
Message: err.Error(),
}
}
yamlSpec = []byte(*request.Body.Spec)
} else {
// Default minimal spec
yamlSpec = []byte(`steps:
- command: echo hello
`)
}
if err := a.dagStore.Create(ctx, request.Body.Name, yamlSpec); err != nil {
if errors.Is(err, execution.ErrDAGAlreadyExists) {
return nil, &Error{
HTTPStatus: http.StatusConflict,
Code: api.ErrorCodeAlreadyExists,
}
}
return nil, fmt.Errorf("error creating DAG: %w", err)
}
return &api.CreateNewDAG201JSONResponse{
Name: request.Body.Name,
}, nil
}
func (a *API) DeleteDAG(ctx context.Context, request api.DeleteDAGRequestObject) (api.DeleteDAGResponseObject, error) {
if err := a.isAllowed(config.PermissionWriteDAGs); err != nil {
return nil, err
}
if err := a.requireWrite(ctx); err != nil {
return nil, err
}
_, err := a.dagStore.GetMetadata(ctx, request.FileName)
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", request.FileName),
}
}
if err := a.dagStore.Delete(ctx, request.FileName); err != nil {
return nil, fmt.Errorf("error deleting DAG: %w", err)
}
return &api.DeleteDAG204Response{}, nil
}
func (a *API) GetDAGSpec(ctx context.Context, request api.GetDAGSpecRequestObject) (api.GetDAGSpecResponseObject, error) {
yamlSpec, err := a.dagStore.GetSpec(ctx, request.FileName)
if err != nil {
return nil, err
}
// Validate the spec - use WithAllowBuildErrors to return DAG even with errors
dag, err := spec.LoadYAML(ctx,
[]byte(yamlSpec),
spec.WithName(request.FileName),
spec.WithAllowBuildErrors(),
spec.WithoutEval(),
)
var errs []string
var loadErrs core.ErrorList
if errors.As(err, &loadErrs) {
errs = loadErrs.ToStringList()
} else if err != nil {
// If we still get an error with AllowBuildErrors, something is seriously wrong
return nil, err
}
// If dag is still nil (shouldn't happen with AllowBuildErrors), create a minimal DAG
if dag == nil {
dag = &core.DAG{
Name: request.FileName,
}
if err != nil {
errs = append(errs, err.Error())
}
} else {
for _, buildErr := range dag.BuildErrors {
errs = append(errs, buildErr.Error())
}
errs = append(errs, dag.BuildWarnings...)
}
return &api.GetDAGSpec200JSONResponse{
Dag: toDAGDetails(dag),
Spec: yamlSpec,
Errors: errs,
}, nil
}
func (a *API) UpdateDAGSpec(ctx context.Context, request api.UpdateDAGSpecRequestObject) (api.UpdateDAGSpecResponseObject, error) {
if err := a.isAllowed(config.PermissionWriteDAGs); err != nil {
return nil, err
}
if err := a.requireWrite(ctx); err != nil {
return nil, err
}
err := a.dagStore.UpdateSpec(ctx, request.FileName, []byte(request.Body.Spec))
var loadErrs core.ErrorList
var errs []string
if errors.As(err, &loadErrs) {
errs = loadErrs.ToStringList()
} else {
return nil, err
}
return api.UpdateDAGSpec200JSONResponse{
Errors: errs,
}, nil
}
func (a *API) RenameDAG(ctx context.Context, request api.RenameDAGRequestObject) (api.RenameDAGResponseObject, error) {
if err := a.isAllowed(config.PermissionWriteDAGs); err != nil {
return nil, err
}
if err := a.requireWrite(ctx); err != nil {
return nil, err
}
if err := core.ValidateDAGName(request.Body.NewFileName); err != nil {
return nil, &Error{
HTTPStatus: http.StatusBadRequest,
Code: api.ErrorCodeBadRequest,
Message: err.Error(),
}
}
dag, err := a.dagStore.GetMetadata(ctx, request.FileName)
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", request.FileName),
}
}
dagStatus, err := a.dagRunMgr.GetLatestStatus(ctx, dag)
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", request.FileName),
}
}
if dagStatus.Status == core.Running {
return nil, &Error{
HTTPStatus: http.StatusBadRequest,
Code: api.ErrorCodeNotRunning,
Message: "DAG is running",
}
}
if err := a.dagStore.Rename(ctx, request.FileName, request.Body.NewFileName); err != nil {
return nil, fmt.Errorf("failed to move DAG: %w", err)
}
return api.RenameDAG200Response{}, nil
}
func (a *API) GetDAGDAGRunHistory(ctx context.Context, request api.GetDAGDAGRunHistoryRequestObject) (api.GetDAGDAGRunHistoryResponseObject, error) {
// Try to get metadata, but if it fails (e.g., due to errors), use the fileName as the DAG name
dag, err := a.dagStore.GetMetadata(ctx, request.FileName)
var dagName string
if err != nil {
// For DAGs with errors, we can still try to get history using the fileName as the name
dagName = request.FileName
} else {
dagName = dag.Name
}
defaultHistoryLimit := 30
recentHistory := a.dagRunMgr.ListRecentStatus(ctx, dagName, defaultHistoryLimit)
var dagRuns []api.DAGRunDetails
for _, status := range recentHistory {
dagRuns = append(dagRuns, toDAGRunDetails(status))
}
gridData := a.readHistoryData(ctx, recentHistory)
return api.GetDAGDAGRunHistory200JSONResponse{
DagRuns: dagRuns,
GridData: gridData,
}, nil
}
func (a *API) GetDAGDetails(ctx context.Context, request api.GetDAGDetailsRequestObject) (api.GetDAGDetailsResponseObject, error) {
fileName := request.FileName
dag, err := a.dagStore.GetDetails(ctx, fileName, spec.WithAllowBuildErrors())
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", fileName),
}
}
dagStatus, err := a.dagRunMgr.GetLatestStatus(ctx, dag)
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", fileName),
}
}
details := toDAGDetails(dag)
var localDAGs []api.LocalDag
for _, localDAG := range dag.LocalDAGs {
localDAGs = append(localDAGs, toLocalDAG(localDAG))
}
// sort localDAGs by name
sort.Slice(localDAGs, func(i, j int) bool {
return strings.Compare(localDAGs[i].Name, localDAGs[j].Name) <= 0
})
// Extract build errors if any
var errs []string
if len(dag.BuildErrors) > 0 {
for _, buildErr := range dag.BuildErrors {
errs = append(errs, buildErr.Error())
}
}
return api.GetDAGDetails200JSONResponse{
Dag: details,
LatestDAGRun: toDAGRunDetails(dagStatus),
Suspended: a.dagStore.IsSuspended(ctx, fileName),
LocalDags: localDAGs,
Errors: errs,
}, nil
}
func (a *API) readHistoryData(
_ context.Context,
statusList []execution.DAGRunStatus,
) []api.DAGGridItem {
data := map[string][]core.NodeStatus{}
addStatusFn := func(
data map[string][]core.NodeStatus,
logLen int,
logIdx int,
nodeName string,
nodeStatus core.NodeStatus,
) {
if _, ok := data[nodeName]; !ok {
data[nodeName] = make([]core.NodeStatus, logLen)
}
data[nodeName][logIdx] = nodeStatus
}
for idx, st := range statusList {
for _, node := range st.Nodes {
addStatusFn(data, len(statusList), idx, node.Step.Name, node.Status)
}
}
var grid []api.DAGGridItem
for node, statusList := range data {
var history []api.NodeStatus
for _, s := range statusList {
history = append(history, api.NodeStatus(s))
}
grid = append(grid, api.DAGGridItem{
Name: node,
History: history,
})
}
sort.Slice(grid, func(i, j int) bool {
return strings.Compare(grid[i].Name, grid[j].Name) <= 0
})
handlers := map[string][]core.NodeStatus{}
for idx, status := range statusList {
if n := status.OnSuccess; n != nil {
addStatusFn(handlers, len(statusList), idx, n.Step.Name, n.Status)
}
if n := status.OnFailure; n != nil {
addStatusFn(handlers, len(statusList), idx, n.Step.Name, n.Status)
}
if n := status.OnCancel; n != nil {
addStatusFn(handlers, len(statusList), idx, n.Step.Name, n.Status)
}
if n := status.OnExit; n != nil {
addStatusFn(handlers, len(statusList), idx, n.Step.Name, n.Status)
}
}
for _, handlerType := range []core.HandlerType{
core.HandlerOnSuccess,
core.HandlerOnFailure,
core.HandlerOnCancel,
core.HandlerOnExit,
} {
if statusList, ok := handlers[handlerType.String()]; ok {
var history []api.NodeStatus
for _, status := range statusList {
history = append(history, api.NodeStatus(status))
}
grid = append(grid, api.DAGGridItem{
Name: handlerType.String(),
History: history,
})
}
}
return grid
}
func (a *API) ListDAGs(ctx context.Context, request api.ListDAGsRequestObject) (api.ListDAGsResponseObject, error) {
// Extract sort and order parameters with config defaults
sortField := a.config.UI.DAGs.SortField
if sortField == "" {
sortField = "name" // fallback if config is empty
}
if request.Params.Sort != nil {
sortField = string(*request.Params.Sort)
}
sortOrder := a.config.UI.DAGs.SortOrder
if sortOrder == "" {
sortOrder = "asc" // fallback if config is empty
}
if request.Params.Order != nil {
sortOrder = string(*request.Params.Order)
}
// Use paginator from request
pg := execution.NewPaginator(valueOf(request.Params.Page), valueOf(request.Params.PerPage))
// Let persistence layer handle sorting and pagination
result, errList, err := a.dagStore.List(ctx, execution.ListDAGsOptions{
Paginator: &pg,
Name: valueOf(request.Params.Name),
Tag: valueOf(request.Params.Tag),
Sort: sortField,
Order: sortOrder,
})
if err != nil {
return nil, fmt.Errorf("error listing DAGs: %w", err)
}
// Build DAG files for the paginated results
dagFiles := make([]api.DAGFile, 0, len(result.Items))
for _, item := range result.Items {
dagStatus, err := a.dagRunMgr.GetLatestStatus(ctx, item)
if err != nil {
errList = append(errList, err.Error())
}
suspended := a.dagStore.IsSuspended(ctx, item.FileName())
dagRun := toDAGRunSummary(dagStatus)
// Include any build errors from the DAG
var dagErrors []string
if item.BuildErrors != nil {
for _, err := range item.BuildErrors {
dagErrors = append(dagErrors, err.Error())
}
}
dagFile := api.DAGFile{
FileName: item.FileName(),
LatestDAGRun: dagRun,
Suspended: suspended,
Dag: toDAG(item),
Errors: dagErrors,
}
dagFiles = append(dagFiles, dagFile)
}
resp := &api.ListDAGs200JSONResponse{
Dags: dagFiles,
Errors: errList,
Pagination: toPagination(result),
}
return resp, nil
}
func (a *API) GetAllDAGTags(ctx context.Context, _ api.GetAllDAGTagsRequestObject) (api.GetAllDAGTagsResponseObject, error) {
tags, errs, err := a.dagStore.TagList(ctx)
if err != nil {
return nil, fmt.Errorf("error getting tags: %w", err)
}
return &api.GetAllDAGTags200JSONResponse{
Tags: tags,
Errors: errs,
}, nil
}
func (a *API) GetDAGDAGRunDetails(ctx context.Context, request api.GetDAGDAGRunDetailsRequestObject) (api.GetDAGDAGRunDetailsResponseObject, error) {
dagFileName := request.FileName
dagRunId := request.DagRunId
// Try to get metadata first
dag, err := a.dagStore.GetMetadata(ctx, dagFileName)
if err != nil {
// For DAGs with errors, try to load with AllowBuildErrors
dag, err = a.dagStore.GetDetails(ctx, dagFileName, spec.WithAllowBuildErrors())
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", dagFileName),
}
}
}
if dagRunId == "latest" {
latestStatus, err := a.dagRunMgr.GetLatestStatus(ctx, dag)
if err != nil {
return nil, fmt.Errorf("error getting latest status: %w", err)
}
return &api.GetDAGDAGRunDetails200JSONResponse{
DagRun: toDAGRunDetails(latestStatus),
}, nil
}
dagStatus, err := a.dagRunMgr.GetCurrentStatus(ctx, dag, dagRunId)
if err != nil {
if errors.Is(err, execution.ErrNoStatusData) {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG run %s not found", dagRunId),
}
}
return nil, fmt.Errorf("error getting status by dag-run ID: %w", err)
}
return &api.GetDAGDAGRunDetails200JSONResponse{
DagRun: toDAGRunDetails(*dagStatus),
}, nil
}
func (a *API) ExecuteDAG(ctx context.Context, request api.ExecuteDAGRequestObject) (api.ExecuteDAGResponseObject, error) {
if err := a.isAllowed(config.PermissionRunDAGs); err != nil {
return nil, err
}
if err := a.requireExecute(ctx); err != nil {
return nil, err
}
dag, err := a.dagStore.GetDetails(ctx, request.FileName)
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", request.FileName),
}
}
var dagRunId, params string
var singleton bool
var nameOverride string
if request.Body != nil {
dagRunId = valueOf(request.Body.DagRunId)
params = valueOf(request.Body.Params)
singleton = valueOf(request.Body.Singleton)
nameOverride = strings.TrimSpace(valueOf(request.Body.DagName))
}
if nameOverride != "" {
if err := core.ValidateDAGName(nameOverride); err != nil {
return nil, &Error{
HTTPStatus: http.StatusBadRequest,
Code: api.ErrorCodeBadRequest,
Message: err.Error(),
}
}
dag.Name = nameOverride
}
if dagRunId == "" {
var err error
dagRunId, err = a.dagRunMgr.GenDAGRunID(ctx)
if err != nil {
return nil, fmt.Errorf("error generating dag-run ID: %w", err)
}
}
if singleton {
alive, err := a.procStore.CountAliveByDAGName(ctx, dag.ProcGroup(), dag.Name)
if err != nil {
return nil, fmt.Errorf("failed to check singleton execution status: %w", err)
}
if alive > 0 {
return nil, &Error{
HTTPStatus: http.StatusConflict,
Code: api.ErrorCodeAlreadyExists,
Message: fmt.Sprintf("DAG %s is already running (singleton mode)", dag.Name),
}
}
}
if err := a.ensureDAGRunIDUnique(ctx, dag, dagRunId); err != nil {
return nil, err
}
if err := a.startDAGRun(ctx, dag, params, dagRunId, nameOverride, singleton); err != nil {
return nil, fmt.Errorf("error starting dag-run: %w", err)
}
return api.ExecuteDAG200JSONResponse{
DagRunId: dagRunId,
}, nil
}
func (a *API) startDAGRun(ctx context.Context, dag *core.DAG, params, dagRunID, nameOverride string, singleton bool) error {
return a.startDAGRunWithOptions(ctx, dag, startDAGRunOptions{
params: params,
dagRunID: dagRunID,
nameOverride: nameOverride,
singleton: singleton,
})
}
// ensureDAGRunIDUnique validates that the given dagRunID is not already in use for this DAG.
func (a *API) ensureDAGRunIDUnique(ctx context.Context, dag *core.DAG, dagRunID string) error {
if dagRunID == "" {
return fmt.Errorf("dagRunID must be non-empty")
}
if _, err := a.dagRunStore.FindAttempt(ctx, execution.NewDAGRunRef(dag.Name, dagRunID)); err == nil {
return &Error{
HTTPStatus: http.StatusConflict,
Code: api.ErrorCodeAlreadyExists,
Message: fmt.Sprintf("dag-run ID %s already exists for DAG %s", dagRunID, dag.Name),
}
} else if !errors.Is(err, execution.ErrDAGRunIDNotFound) {
return fmt.Errorf("failed to verify dag-run ID uniqueness: %w", err)
}
return nil
}
type startDAGRunOptions struct {
params string
dagRunID string
nameOverride string
singleton bool
fromRunID string
target string
}
func (a *API) startDAGRunWithOptions(ctx context.Context, dag *core.DAG, opts startDAGRunOptions) error {
spec := a.subCmdBuilder.Start(dag, runtime1.StartOptions{
Params: opts.params,
DAGRunID: opts.dagRunID,
Quiet: true,
NameOverride: opts.nameOverride,
FromRunID: opts.fromRunID,
Target: opts.target,
})
if err := runtime1.Start(ctx, spec); err != nil {
return fmt.Errorf("error starting DAG: %w", err)
}
// Wait for the DAG to start
// Use longer timeout on Windows due to slower process startup
timeout := 5 * time.Second // default timeout
if runtime.GOOS == "windows" {
timeout = 10 * time.Second
}
timer := time.NewTimer(timeout)
var running bool
defer timer.Stop()
waitLoop:
for {
select {
case <-timer.C:
break waitLoop
case <-ctx.Done():
break waitLoop
default:
dagStatus, _ := a.dagRunMgr.GetCurrentStatus(ctx, dag, opts.dagRunID)
if dagStatus == nil {
continue
}
if dagStatus.Status != core.NotStarted {
// If status is not NotStarted, it means the DAG has started or even finished
running = true
timer.Stop()
break waitLoop
}
time.Sleep(100 * time.Millisecond)
}
}
if !running {
return &Error{
HTTPStatus: http.StatusInternalServerError,
Code: api.ErrorCodeInternalError,
Message: "DAG did not start",
}
}
return nil
}
func (a *API) EnqueueDAGDAGRun(ctx context.Context, request api.EnqueueDAGDAGRunRequestObject) (api.EnqueueDAGDAGRunResponseObject, error) {
if err := a.isAllowed(config.PermissionRunDAGs); err != nil {
return nil, err
}
if err := a.requireExecute(ctx); err != nil {
return nil, err
}
dag, err := a.dagStore.GetDetails(ctx, request.FileName, spec.WithoutEval())
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", request.FileName),
}
}
// Apply queue override if provided
if request.Body != nil && request.Body.Queue != nil && *request.Body.Queue != "" {
dag.Queue = *request.Body.Queue
}
var nameOverride string
if request.Body != nil {
nameOverride = strings.TrimSpace(valueOf(request.Body.DagName))
}
if nameOverride != "" {
if err := core.ValidateDAGName(nameOverride); err != nil {
return nil, &Error{
HTTPStatus: http.StatusBadRequest,
Code: api.ErrorCodeBadRequest,
Message: err.Error(),
}
}
dag.Name = nameOverride
}
dagRunId := valueOf(request.Body.DagRunId)
if dagRunId == "" {
var err error
dagRunId, err = a.dagRunMgr.GenDAGRunID(ctx)
if err != nil {
return nil, fmt.Errorf("error generating dag-run ID: %w", err)
}
}
singleton := valueOf(request.Body.Singleton)
if singleton {
// Check if running
alive, err := a.procStore.CountAliveByDAGName(ctx, dag.ProcGroup(), dag.Name)
if err != nil {
return nil, fmt.Errorf("failed to check singleton execution status (proc): %w", err)
}
if alive > 0 {
return nil, &Error{
HTTPStatus: http.StatusConflict,
Code: api.ErrorCodeAlreadyExists,
Message: fmt.Sprintf("DAG %s is already running (singleton mode)", dag.Name),
}
}
// Check if queued
queued, err := a.queueStore.ListByDAGName(ctx, dag.ProcGroup(), dag.Name)
if err != nil {
return nil, fmt.Errorf("failed to check singleton execution status (queue): %w", err)
}
if len(queued) > 0 {
return nil, &Error{
HTTPStatus: http.StatusConflict,
Code: api.ErrorCodeAlreadyExists,
Message: fmt.Sprintf("DAG %s is already in queue (singleton mode)", dag.Name),
}
}
}
if err := a.enqueueDAGRun(ctx, dag, valueOf(request.Body.Params), dagRunId, nameOverride); err != nil {
return nil, fmt.Errorf("error enqueuing dag-run: %w", err)
}
return api.EnqueueDAGDAGRun200JSONResponse{
DagRunId: dagRunId,
}, nil
}
func (a *API) enqueueDAGRun(ctx context.Context, dag *core.DAG, params, dagRunID, nameOverride string) error {
opts := runtime1.EnqueueOptions{
Params: params,
DAGRunID: dagRunID,
NameOverride: nameOverride,
}
if dag.Queue != "" {
opts.Queue = dag.Queue
}
spec := a.subCmdBuilder.Enqueue(dag, opts)
if err := runtime1.Run(ctx, spec); err != nil {
return fmt.Errorf("error enqueuing DAG: %w", err)
}
// Wait for the DAG to be enqueued
timer := time.NewTimer(3 * time.Second)
var ok bool
defer timer.Stop()
waitLoop:
for {
select {
case <-timer.C:
break waitLoop
case <-ctx.Done():
break waitLoop
default:
dagStatus, _ := a.dagRunMgr.GetCurrentStatus(ctx, dag, dagRunID)
if dagStatus == nil {
continue
}
if dagStatus.Status != core.NotStarted {
// If status is not NotStarted, it means the DAG has started or even finished
ok = true
timer.Stop()
break waitLoop
}
time.Sleep(100 * time.Millisecond)
}
}
if !ok {
return &Error{
HTTPStatus: http.StatusInternalServerError,
Code: api.ErrorCodeInternalError,
Message: "Failed to enqueue dagRun execution",
}
}
return nil
}
func (a *API) UpdateDAGSuspensionState(ctx context.Context, request api.UpdateDAGSuspensionStateRequestObject) (api.UpdateDAGSuspensionStateResponseObject, error) {
if err := a.isAllowed(config.PermissionRunDAGs); err != nil {
return nil, err
}
if err := a.requireExecute(ctx); err != nil {
return nil, err
}
_, err := a.dagStore.GetMetadata(ctx, request.FileName)
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", request.FileName),
}
}
if err := a.dagStore.ToggleSuspend(ctx, request.FileName, request.Body.Suspend); err != nil {
return nil, fmt.Errorf("error toggling suspend: %w", err)
}
return api.UpdateDAGSuspensionState200Response{}, nil
}
func (a *API) SearchDAGs(ctx context.Context, request api.SearchDAGsRequestObject) (api.SearchDAGsResponseObject, error) {
ret, errs, err := a.dagStore.Grep(ctx, request.Params.Q)
if err != nil {
return nil, fmt.Errorf("error searching DAGs: %w", err)
}
var results []api.SearchResultItem
for _, item := range ret {
var matches []api.SearchDAGsMatchItem
for _, match := range item.Matches {
matches = append(matches, api.SearchDAGsMatchItem{
Line: match.Line,
LineNumber: match.LineNumber,
StartLine: match.StartLine,
})
}
results = append(results, api.SearchResultItem{
Name: item.Name,
Dag: toDAG(item.DAG),
Matches: matches,
})
}
return &api.SearchDAGs200JSONResponse{
Results: results,
Errors: errs,
}, nil
}
func (a *API) StopAllDAGRuns(ctx context.Context, request api.StopAllDAGRunsRequestObject) (api.StopAllDAGRunsResponseObject, error) {
if err := a.isAllowed(config.PermissionRunDAGs); err != nil {
return nil, err
}
if err := a.requireExecute(ctx); err != nil {
return nil, err
}
// Get the DAG metadata to ensure it exists
dag, err := a.dagStore.GetMetadata(ctx, request.FileName)
if err != nil {
return nil, &Error{
HTTPStatus: http.StatusNotFound,
Code: api.ErrorCodeNotFound,
Message: fmt.Sprintf("DAG %s not found", request.FileName),
}
}
// Get all running DAG-runs for this DAG
runningStatuses, err := a.dagRunStore.ListStatuses(ctx,
execution.WithExactName(dag.Name),
execution.WithStatuses([]core.Status{core.Running}),
)
if err != nil {
return nil, fmt.Errorf("error listing running DAG-runs: %w", err)
}
// Stop each running DAG-run
var errors []string
for _, runningStatus := range runningStatuses {
runID := runningStatus.DAGRunID
err := a.dagRunMgr.Stop(ctx, dag, runID)
if err != nil {
errors = append(errors, fmt.Sprintf("failed to stop run %q: %s", runID, err))
}
if ctx.Err() != nil {
errors = append(errors, fmt.Sprintf("context is cancelled: %s", err))
break
}
}
return &api.StopAllDAGRuns200JSONResponse{
Errors: errors,
}, nil
}