Skip to content

Commit c614266

Browse files
committed
Add --as-job-runner: With this flag hivemind will let processes exit gracefully, and wait for all to complete
See `test-as-job-runner.sh` and: Example: ```shellsession hivemind --as-job-runner --exit-with-highest-exit-code - <<PROCFILE job1: echo "job1 is running"; sleep 0.5; echo "Done" job2: echo "job2 is running"; sleep 1; echo "Done" PROCFILE hivemind --as-job-runner --exit-with-highest-exit-code - <<PROCFILE job1: echo "job1 is running"; sleep 0.5; echo "Fail"; exit 13 job2: echo "job2 is running"; sleep 1; echo "Done" PROCFILE ```
1 parent d8b34a0 commit c614266

4 files changed

Lines changed: 74 additions & 10 deletions

File tree

hivemind.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type hivemindConfig struct {
2121
Timeout int
2222
NoPrefix bool
2323
PrintTimestamps bool
24+
ExitWithHighest bool
25+
AsJobRunner bool
2426
}
2527

2628
type hivemind struct {
@@ -31,6 +33,7 @@ type hivemind struct {
3133
done chan bool
3234
interrupted chan os.Signal
3335
timeout time.Duration
36+
jobRunner bool
3437
}
3538

3639
func newHivemind(conf hivemindConfig) (h *hivemind) {
@@ -43,6 +46,7 @@ func newHivemind(conf hivemindConfig) (h *hivemind) {
4346
}
4447

4548
h.output = &multiOutput{printProcName: !conf.NoPrefix, printTimestamp: conf.PrintTimestamps}
49+
h.jobRunner = conf.AsJobRunner
4650

4751
entries := parseProcfile(conf.Procfile, conf.PortBase, conf.PortStep)
4852
h.procs = make([]*process, 0)
@@ -62,17 +66,32 @@ func (h *hivemind) runProcess(proc *process) {
6266
h.procWg.Add(1)
6367

6468
go func() {
69+
procSucceed := false
70+
6571
defer h.procWg.Done()
66-
defer func() { h.done <- true }()
72+
defer func() { h.done <- procSucceed }()
6773

68-
proc.Run()
74+
procSucceed = proc.Run()
6975
}()
7076
}
7177

72-
func (h *hivemind) waitForDoneOrInterrupt() {
78+
func (h *hivemind) waitForDoneOrInterrupt() bool {
7379
select {
74-
case <-h.done:
80+
case done := <-h.done:
81+
return done
7582
case <-h.interrupted:
83+
return false
84+
}
85+
}
86+
87+
func (h *hivemind) waitForJobsToCompleteOrInterrupt() {
88+
jobsCount := len(h.procs)
89+
90+
for jobsCompleted := 0; jobsCompleted < jobsCount; jobsCompleted++ {
91+
succeeded := h.waitForDoneOrInterrupt()
92+
if !succeeded {
93+
return
94+
}
7695
}
7796
}
7897

@@ -84,7 +103,11 @@ func (h *hivemind) waitForTimeoutOrInterrupt() {
84103
}
85104

86105
func (h *hivemind) waitForExit() {
87-
h.waitForDoneOrInterrupt()
106+
if h.jobRunner {
107+
h.waitForJobsToCompleteOrInterrupt()
108+
} else {
109+
h.waitForDoneOrInterrupt()
110+
}
88111

89112
for _, proc := range h.procs {
90113
go proc.Interrupt()
@@ -97,7 +120,7 @@ func (h *hivemind) waitForExit() {
97120
}
98121
}
99122

100-
func (h *hivemind) Run() {
123+
func (h *hivemind) Run() int {
101124
fmt.Printf("\033]0;%s | hivemind\007", h.title)
102125

103126
h.done = make(chan bool, len(h.procs))
@@ -112,4 +135,15 @@ func (h *hivemind) Run() {
112135
go h.waitForExit()
113136

114137
h.procWg.Wait()
138+
139+
exitCode := 0
140+
141+
for _, proc := range h.procs {
142+
code := proc.ProcessState.ExitCode()
143+
if code > exitCode {
144+
exitCode = code
145+
}
146+
}
147+
148+
return exitCode
115149
}

main.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ func main() {
3030
app.HideHelp = true
3131

3232
app.Flags = []cli.Flag{
33-
cli.StringFlag{Name: "title, w", EnvVar: "HIVEMIND_TITLE", Usage: "Specify a title of the application", Destination: &conf.Title},
34-
cli.StringFlag{Name: "processes, l", EnvVar: "HIVEMIND_PROCESSES", Usage: "Specify process names to launch. Divide names with comma", Destination: &conf.ProcNames},
33+
cli.StringFlag{Name: "title, w", EnvVar: "HIVEMIND_TITLE", Usage: "specify a title of the application", Destination: &conf.Title},
34+
cli.StringFlag{Name: "processes, l", EnvVar: "HIVEMIND_PROCESSES", Usage: "specify process names to launch. Divide names with comma", Destination: &conf.ProcNames},
3535
cli.IntFlag{Name: "port, p", EnvVar: "HIVEMIND_PORT,PORT", Usage: "specify a port to use as the base", Value: 5000, Destination: &conf.PortBase},
3636
cli.IntFlag{Name: "port-step, P", EnvVar: "HIVEMIND_PORT_STEP", Usage: "specify a step to increase port number", Value: 100, Destination: &conf.PortStep},
3737
cli.StringFlag{Name: "root, d", EnvVar: "HIVEMIND_ROOT", Usage: "specify a working directory of application. Default: directory containing the Procfile", Destination: &conf.Root},
3838
cli.IntFlag{Name: "timeout, t", EnvVar: "HIVEMIND_TIMEOUT", Usage: "specify the amount of time (in seconds) processes have to shut down gracefully before being brutally killed", Value: 5, Destination: &conf.Timeout},
3939
cli.BoolFlag{Name: "no-prefix", EnvVar: "HIVEMIND_NO_PREFIX", Usage: "process names will not be printed if the flag is specified", Destination: &conf.NoPrefix},
4040
cli.BoolFlag{Name: "print-timestamps, T", EnvVar: "HIVEMIND_PRINT_TIMESTAMPS", Usage: "timestamps will be printed if the flag is specified", Destination: &conf.PrintTimestamps},
41+
cli.BoolFlag{Name: "exit-with-highest-exit-code, e", EnvVar: "HIVEMIND_EXIT_WITH_HIGHEST_EXIT_CODE", Usage: "exit hivemind with highest exit code from all processes", Destination: &conf.ExitWithHighest},
42+
cli.BoolFlag{Name: "as-job-runner", EnvVar: "HIVEMIND_AS_JOB_RUNNER", Usage: "be a job runner instead: Will wait for all to finish with exit code 0, or fail.", Destination: &conf.AsJobRunner},
4143
}
4244

4345
app.Action = func(c *cli.Context) error {
@@ -65,7 +67,10 @@ func main() {
6567
conf.Root, err = filepath.Abs(conf.Root)
6668
fatalOnErr(err)
6769

68-
newHivemind(conf).Run()
70+
exitCode := newHivemind(conf).Run()
71+
if exitCode > 0 && conf.ExitWithHighest {
72+
return cli.NewExitError("At least one process failed", exitCode)
73+
}
6974

7075
return nil
7176
}

process.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (p *process) Running() bool {
5656
return p.Process != nil && p.ProcessState == nil
5757
}
5858

59-
func (p *process) Run() {
59+
func (p *process) Run() bool {
6060
p.output.PipeOutput(p)
6161
defer p.output.ClosePipe(p)
6262

@@ -66,8 +66,10 @@ func (p *process) Run() {
6666

6767
if err := p.Cmd.Run(); err != nil {
6868
p.writeErr(err)
69+
return false
6970
} else {
7071
p.writeLine([]byte("\033[1mProcess exited\033[0m"))
72+
return true
7173
}
7274
}
7375

test-as-job-runner.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
3+
echo
4+
echo "As process manager"
5+
dist/hivemind --exit-with-highest-exit-code - <<PROCFILE
6+
job1: echo "job1 is running"; sleep 0.5; exit 13; echo "Done"
7+
job2: echo "job2 is running"; sleep 2; echo "Done"
8+
PROCFILE
9+
10+
echo
11+
echo "As Job Runner, all completing"
12+
dist/hivemind --as-job-runner --exit-with-highest-exit-code - <<PROCFILE
13+
job1: echo "short job1 is running"; sleep 0.5; echo "Done"
14+
job2: echo "short job2 is running"; sleep 1.0; echo "Done"
15+
job3: echo "long job3 is running"; sleep 2; echo "Done"
16+
PROCFILE
17+
18+
echo
19+
echo "As Job Runner, one exits and fail fast"
20+
dist/hivemind --as-job-runner --exit-with-highest-exit-code - <<PROCFILE
21+
job1: echo "job1 is running"; sleep 0.5; echo "Fail"; exit 13
22+
job2: echo "job2 is running"; sleep 0.5; echo 1; sleep 0.5; echo 2; sleep 0.5; echo 3; sleep 1; echo "Done"
23+
PROCFILE

0 commit comments

Comments
 (0)