Skip to content

Commit f39ea25

Browse files
committed
Added planner implementation
1 parent ffcebc5 commit f39ea25

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

executor/planner.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package executor
2+
3+
import "fmt"
4+
5+
type Command struct {
6+
name string
7+
command string
8+
args map[string]interface{}
9+
cwd string
10+
needs []string
11+
}
12+
13+
type status int
14+
15+
const (
16+
commandPending status = iota
17+
commandRunning
18+
commandSuccess
19+
commandFailed
20+
)
21+
22+
type CommandRun struct {
23+
command Command
24+
status
25+
}
26+
27+
type Executor struct {
28+
commands map[string]*CommandRun
29+
}
30+
31+
type CommandResult struct {
32+
CommandName string
33+
Success bool
34+
Error error
35+
}
36+
37+
func NewExecutor(commands []Command) *Executor {
38+
commandRuns := make(map[string]*CommandRun)
39+
for i, cmd := range commands {
40+
commandRuns[cmd.name] = &CommandRun{
41+
command: cmd,
42+
status: commandPending,
43+
}
44+
}
45+
46+
return &Executor{
47+
commands: commandRuns,
48+
}
49+
}
50+
51+
func (e *Executor) Execute() error {
52+
// Make channel for receiving command completion notifications
53+
completionCh := make(chan CommandResult)
54+
55+
// Find all commands without dependencies, and start executing them
56+
err := e.triggerExecutionOfReadyCommands(completionCh)
57+
if err != nil {
58+
return err
59+
}
60+
61+
if err := e.ensureAtLeastOneCommandRunning(); err != nil {
62+
return err
63+
}
64+
65+
// Wait for any command to finish. If a command finishes, check for new commands that can be executed until all commands are done.
66+
finishedCommands := 0
67+
totalCommands := len(e.commands)
68+
for finishedCommands < totalCommands {
69+
// Wait for a command to complete
70+
result := <-completionCh
71+
finishedCommands++
72+
73+
// Update command status
74+
cmdRun, exists := e.commands[result.CommandName]
75+
if !exists {
76+
return fmt.Errorf("received completion for unknown command: %s", result.CommandName)
77+
}
78+
79+
if result.Success {
80+
cmdRun.status = commandSuccess
81+
} else {
82+
cmdRun.status = commandFailed
83+
}
84+
85+
// Check for new commands that can be executed
86+
err := e.triggerExecutionOfReadyCommands(completionCh)
87+
if err != nil {
88+
return err
89+
}
90+
91+
if finishedCommands == totalCommands {
92+
break
93+
}
94+
95+
if err := e.ensureAtLeastOneCommandRunning(); err != nil {
96+
return err
97+
}
98+
}
99+
100+
return nil
101+
}
102+
103+
// triggerExecutionOfReadyCommands finds and starts execution of all commands whose dependencies are met.
104+
func (e *Executor) triggerExecutionOfReadyCommands(completionCh chan<- CommandResult) error {
105+
for name, cmdRun := range e.commands {
106+
if cmdRun.status != commandPending {
107+
continue
108+
}
109+
110+
// Check if all dependencies are met
111+
depsMet := true
112+
for _, depName := range cmdRun.command.needs {
113+
depCmdRun, exists := e.commands[depName]
114+
if !exists {
115+
return fmt.Errorf("command %s has unknown dependency: %s", name, depName)
116+
}
117+
if depCmdRun.status != commandSuccess {
118+
depsMet = false
119+
break
120+
}
121+
}
122+
123+
if depsMet {
124+
// Start executing the command
125+
cmdRun.status = commandRunning
126+
go func(cmd Command) {
127+
res, err := ExecuteSyncInDirectory(cmd.cwd, cmd.command, ...cmd.args)
128+
129+
if err != nil || res.ExitCode != 0 {
130+
completionCh <- CommandResult{
131+
CommandName: cmd.name,
132+
Success: false,
133+
Error: fmt.Errorf("command %s failed: %v)", cmd.name, err),
134+
}
135+
return
136+
}
137+
completionCh <- CommandResult{
138+
CommandName: cmd.name,
139+
Success: true,
140+
Error: nil,
141+
}
142+
}(cmdRun.command)
143+
}
144+
}
145+
146+
return nil
147+
}
148+
149+
func (e *Executor) ensureAtLeastOneCommandRunning() error {
150+
for _, cmdRun := range e.commands {
151+
if cmdRun.status == commandRunning {
152+
return nil
153+
}
154+
}
155+
156+
return fmt.Errorf("no commands are running, potential deadlock detected. Pending commands: %v", e.getPendingCommands())
157+
}
158+
159+
// getPendingCommands returns a list of names of commands that are still pending.
160+
func (e *Executor) getPendingCommands() []string {
161+
var pending []string
162+
for name, cmdRun := range e.commands {
163+
if cmdRun.status == commandPending {
164+
pending = append(pending, name)
165+
}
166+
}
167+
return pending
168+
}

0 commit comments

Comments
 (0)