dagu/internal/digraph/scheduler/data.go
2025-05-06 14:47:19 +09:00

392 lines
7.5 KiB
Go

package scheduler
import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/dagu-org/dagu/internal/digraph"
"github.com/dagu-org/dagu/internal/stringutil"
"github.com/google/uuid"
)
// Data is a thread-safe wrapper around NodeData.
type Data struct {
mu sync.RWMutex
inner NodeData
}
// NodeData represents the data of a node.
type NodeData struct {
Step digraph.Step
State NodeState
}
type NodeState struct {
// Status represents the state of the node.
Status NodeStatus
// Log is the log file path from the node.
Log string
// StartedAt is the time when the node started.
StartedAt time.Time
// FinishedAt is the time when the node finished.
FinishedAt time.Time
// RetryCount is the number of retries happened based on the retry policy.
RetryCount int
// RetriedAt is the time when the node was retried last time.
RetriedAt time.Time
// DoneCount is the number of times the node was executed.
DoneCount int
// Error is the error that the executor encountered.
Error error
// ExitCode is the exit code that the command exited with.
// It only makes sense when the node is a command executor.
ExitCode int
// SubRuns is the list of sub-runs that this node has executed.
SubRuns []SubRun
}
type SubRun struct {
// RequestID is the request ID of the sub-run.
RequestID string
}
type NodeStatus int
const (
NodeStatusNone NodeStatus = iota
NodeStatusRunning
NodeStatusError
NodeStatusCancel
NodeStatusSuccess
NodeStatusSkipped
)
func (s NodeStatus) String() string {
switch s {
case NodeStatusRunning:
return "running"
case NodeStatusError:
return "failed"
case NodeStatusCancel:
return "canceled"
case NodeStatusSuccess:
return "finished"
case NodeStatusSkipped:
return "skipped"
case NodeStatusNone:
fallthrough
default:
return "not started"
}
}
func newSafeData(data NodeData) Data {
return Data{inner: data}
}
func (s *Data) ResetError() {
s.mu.Lock()
defer s.mu.Unlock()
s.inner.State.Error = nil
s.inner.State.ExitCode = 0
}
func (s *Data) Args() []string {
s.mu.RLock()
defer s.mu.RUnlock()
args := make([]string, len(s.inner.Step.Args))
copy(args, s.inner.Step.Args)
return args
}
func (s *Data) SetArgs(args []string) {
s.mu.Lock()
defer s.mu.Unlock()
s.inner.Step.Args = args
}
func (s *Data) Step() digraph.Step {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.Step
}
func (s *Data) SetStep(step digraph.Step) {
// TODO: refactor to avoid modifying the step
s.mu.Lock()
defer s.mu.Unlock()
s.inner.Step = step
}
func (s *Data) Data() NodeData {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner
}
func (s *Data) SubRunRequestID() (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
// If subRuns is not empty, return the first child's request ID.
if len(s.inner.State.SubRuns) > 0 {
return s.inner.State.SubRuns[0].RequestID, nil
}
// Generate a new request ID for the current node.
r, err := generateRequestID()
if err != nil {
return "", fmt.Errorf("failed to generate request ID: %w", err)
}
s.inner.State.SubRuns = append(s.inner.State.SubRuns, SubRun{RequestID: r})
return r, nil
}
func (s *Data) Setup(ctx context.Context, logFile string, startedAt time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
s.inner.State.Log = logFile
s.inner.State.StartedAt = startedAt
c := digraph.GetExecContext(ctx)
// Evaluate the stdout and stderr fields
stdout, err := c.EvalString(ctx, s.inner.Step.Stdout)
if err != nil {
return fmt.Errorf("failed to evaluate stdout field: %w", err)
}
s.inner.Step.Stdout = stdout
stderr, err := c.EvalString(ctx, s.inner.Step.Stderr)
if err != nil {
return fmt.Errorf("failed to evaluate stderr field: %w", err)
}
s.inner.Step.Stderr = stderr
// Evaluate the dir field
dir, err := c.EvalString(ctx, s.inner.Step.Dir)
if err != nil {
return fmt.Errorf("failed to evaluate dir field: %w", err)
}
s.inner.Step.Dir = dir
return nil
}
func (s *Data) State() NodeState {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.State
}
func (s *Data) Status() NodeStatus {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.State.Status
}
func (s *Data) SetStatus(status NodeStatus) {
s.mu.Lock()
defer s.mu.Unlock()
s.inner.State.Status = status
}
func (s *Data) ContinueOn() digraph.ContinueOn {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.Step.ContinueOn
}
func (s *Data) Log() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.State.Log
}
func (s *Data) SignalOnStop() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.Step.SignalOnStop
}
func (s *Data) Name() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.Step.Name
}
func (s *Data) Error() error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.State.Error
}
func (s *Data) SetError(err error) {
s.mu.Lock()
defer s.mu.Unlock()
s.inner.State.Error = err
}
func (s *Data) ClearVariable(key string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.inner.Step.OutputVariables == nil {
return
}
s.inner.Step.OutputVariables.Delete(key)
}
func (s *Data) MatchExitCode(exitCodes []int) bool {
s.mu.RLock()
defer s.mu.RUnlock()
for _, code := range exitCodes {
if code == s.inner.State.ExitCode {
return true
}
}
return false
}
func (n *Data) getVariable(key string) (stringutil.KeyValue, bool) {
n.mu.RLock()
defer n.mu.RUnlock()
if n.inner.Step.OutputVariables == nil {
return "", false
}
v, ok := n.inner.Step.OutputVariables.Load(key)
if !ok {
return "", false
}
return stringutil.KeyValue(v.(string)), true
}
func (n *Data) getBoolVariable(key string) (bool, bool) {
v, ok := n.getVariable(key)
if !ok {
return false, false
}
return v.Bool(), true
}
func (n *Data) setBoolVariable(key string, value bool) {
if n.inner.Step.OutputVariables == nil {
n.inner.Step.OutputVariables = &digraph.SyncMap{}
}
n.inner.Step.OutputVariables.Store(key, stringutil.NewKeyValue(key, strconv.FormatBool(value)).String())
}
func (n *Data) setVariable(key, value string) {
if n.inner.Step.OutputVariables == nil {
n.inner.Step.OutputVariables = &digraph.SyncMap{}
}
n.inner.Step.OutputVariables.Store(key, stringutil.NewKeyValue(key, value).String())
}
func (n *Data) Finish() {
n.mu.Lock()
defer n.mu.Unlock()
n.inner.State.FinishedAt = time.Now()
}
func (n *Data) IncRetryCount() {
n.mu.Lock()
defer n.mu.Unlock()
n.inner.State.RetryCount++
}
func (n *Data) GetRetryCount() int {
n.mu.RLock()
defer n.mu.RUnlock()
return n.inner.State.RetryCount
}
func (n *Data) SetRetriedAt(retriedAt time.Time) {
n.mu.Lock()
defer n.mu.Unlock()
n.inner.State.RetriedAt = retriedAt
}
func (n *Data) IncDoneCount() {
n.mu.Lock()
defer n.mu.Unlock()
n.inner.State.DoneCount++
}
func (n *Data) GetDoneCount() int {
n.mu.RLock()
defer n.mu.RUnlock()
return n.inner.State.DoneCount
}
func (n *Data) GetExitCode() int {
n.mu.RLock()
defer n.mu.RUnlock()
return n.inner.State.ExitCode
}
func (n *Data) SetExitCode(exitCode int) {
n.mu.Lock()
defer n.mu.Unlock()
n.inner.State.ExitCode = exitCode
}
func (n *Data) ClearState() {
n.mu.Lock()
defer n.mu.Unlock()
// The data of sub-runs need to be preserved to retain their request IDs
subRuns := n.inner.State.SubRuns
n.inner.State = NodeState{}
n.inner.State.SubRuns = subRuns
}
func (n *Data) MarkError(err error) {
n.mu.Lock()
defer n.mu.Unlock()
n.inner.State.Error = err
n.inner.State.Status = NodeStatusError
}
// generateRequestID generates a new request ID.
// For simplicity, we use UUIDs as request IDs.
func generateRequestID() (string, error) {
id, err := uuid.NewV7()
if err != nil {
return "", err
}
return id.String(), nil
}