-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcancel.go
More file actions
147 lines (133 loc) · 4.51 KB
/
Copy pathcancel.go
File metadata and controls
147 lines (133 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package cli
import (
"database/sql"
"fmt"
"time"
"github.com/dvflw/mantle/internal/audit"
"github.com/dvflw/mantle/internal/config"
"github.com/dvflw/mantle/internal/db"
"github.com/spf13/cobra"
)
// childrenCTE is a recursive CTE that selects an execution and all its descendants.
const childrenCTE = `WITH RECURSIVE children AS (
SELECT id FROM workflow_executions WHERE id = $1
UNION ALL
SELECT e.id FROM workflow_executions e
JOIN children c ON e.parent_execution_id = c.id
)`
func newCancelCommand() *cobra.Command {
return &cobra.Command{
Use: "cancel <execution-id>",
Short: "Cancel a running workflow",
Long: "Cancels a running workflow execution. The execution is marked as cancelled.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
execID := args[0]
cfg := config.FromContext(cmd.Context())
if cfg == nil {
return fmt.Errorf("config not loaded")
}
database, err := db.Open(cfg.Database)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer database.Close()
tx, err := database.BeginTx(cmd.Context(), nil)
if err != nil {
return fmt.Errorf("starting transaction: %w", err)
}
defer tx.Rollback() //nolint:errcheck
// Cancel the execution and all child executions recursively.
// Use RETURNING id to capture which executions were cancelled.
rows, err := tx.QueryContext(cmd.Context(),
childrenCTE+`
UPDATE workflow_executions SET status = 'cancelled', completed_at = NOW(), updated_at = NOW()
WHERE id IN (SELECT id FROM children) AND status IN ('pending', 'running', 'queued')
RETURNING id`,
execID,
)
if err != nil {
return fmt.Errorf("cancelling execution: %w", err)
}
var cancelledIDs []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
rows.Close()
return fmt.Errorf("scanning cancelled id: %w", err)
}
cancelledIDs = append(cancelledIDs, id)
}
rows.Close()
if err := rows.Err(); err != nil {
return fmt.Errorf("iterating cancelled ids: %w", err)
}
if len(cancelledIDs) == 0 {
// Check if execution exists at all (use tx for consistent read).
var status string
err := tx.QueryRowContext(cmd.Context(),
`SELECT status FROM workflow_executions WHERE id = $1`, execID,
).Scan(&status)
if err == sql.ErrNoRows {
return fmt.Errorf("execution %q not found", execID)
}
if err != nil {
return fmt.Errorf("checking execution status: %w", err)
}
fmt.Fprintf(cmd.OutOrStdout(), "Execution %s is already %s\n", execID, status)
return nil
}
// Also mark any running/pending steps in the tree as cancelled,
// collecting their IDs for audit events.
stepRows, err := tx.QueryContext(cmd.Context(),
childrenCTE+`
UPDATE step_executions SET status = 'cancelled', completed_at = NOW(), updated_at = NOW()
WHERE execution_id IN (SELECT id FROM children) AND status IN ('pending', 'running')
RETURNING id`,
execID,
)
if err != nil {
return fmt.Errorf("cancelling step executions: %w", err)
}
var cancelledStepIDs []string
for stepRows.Next() {
var id string
if err := stepRows.Scan(&id); err != nil {
stepRows.Close()
return fmt.Errorf("scanning cancelled step id: %w", err)
}
cancelledStepIDs = append(cancelledStepIDs, id)
}
stepRows.Close()
if err := stepRows.Err(); err != nil {
return fmt.Errorf("iterating cancelled step ids: %w", err)
}
// Emit audit events inside the transaction so they commit atomically.
for _, id := range cancelledIDs {
if err := audit.EmitTx(cmd.Context(), tx, audit.Event{
Timestamp: time.Now(),
Actor: "cli",
Action: audit.ActionExecutionCancelled,
Resource: audit.Resource{Type: "workflow_execution", ID: id},
}); err != nil {
return fmt.Errorf("emitting audit event for %s: %w", id, err)
}
}
for _, id := range cancelledStepIDs {
if err := audit.EmitTx(cmd.Context(), tx, audit.Event{
Timestamp: time.Now(),
Actor: "cli",
Action: audit.ActionExecutionCancelled,
Resource: audit.Resource{Type: "step_execution", ID: id},
}); err != nil {
return fmt.Errorf("emitting audit event for step %s: %w", id, err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing cancellation: %w", err)
}
fmt.Fprintf(cmd.OutOrStdout(), "Cancelled execution %s (%d executions affected)\n", execID, len(cancelledIDs))
return nil
},
}
}