Skip to content

Commit

Permalink
Add --as-job-runner: With this flag hivemind will let processes exi…
Browse files Browse the repository at this point in the history
…t 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
```
  • Loading branch information
danieroux committed Oct 18, 2024
1 parent d8b34a0 commit c614266
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 10 deletions.
46 changes: 40 additions & 6 deletions hivemind.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type hivemindConfig struct {
Timeout int
NoPrefix bool
PrintTimestamps bool
ExitWithHighest bool
AsJobRunner bool
}

type hivemind struct {
Expand All @@ -31,6 +33,7 @@ type hivemind struct {
done chan bool
interrupted chan os.Signal
timeout time.Duration
jobRunner bool
}

func newHivemind(conf hivemindConfig) (h *hivemind) {
Expand All @@ -43,6 +46,7 @@ func newHivemind(conf hivemindConfig) (h *hivemind) {
}

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

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

go func() {
procSucceed := false

defer h.procWg.Done()
defer func() { h.done <- true }()
defer func() { h.done <- procSucceed }()

proc.Run()
procSucceed = proc.Run()
}()
}

func (h *hivemind) waitForDoneOrInterrupt() {
func (h *hivemind) waitForDoneOrInterrupt() bool {
select {
case <-h.done:
case done := <-h.done:
return done
case <-h.interrupted:
return false
}
}

func (h *hivemind) waitForJobsToCompleteOrInterrupt() {
jobsCount := len(h.procs)

for jobsCompleted := 0; jobsCompleted < jobsCount; jobsCompleted++ {
succeeded := h.waitForDoneOrInterrupt()
if !succeeded {
return
}
}
}

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

func (h *hivemind) waitForExit() {
h.waitForDoneOrInterrupt()
if h.jobRunner {
h.waitForJobsToCompleteOrInterrupt()
} else {
h.waitForDoneOrInterrupt()
}

for _, proc := range h.procs {
go proc.Interrupt()
Expand All @@ -97,7 +120,7 @@ func (h *hivemind) waitForExit() {
}
}

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

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

h.procWg.Wait()

exitCode := 0

for _, proc := range h.procs {
code := proc.ProcessState.ExitCode()
if code > exitCode {
exitCode = code
}
}

return exitCode
}
11 changes: 8 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ func main() {
app.HideHelp = true

app.Flags = []cli.Flag{
cli.StringFlag{Name: "title, w", EnvVar: "HIVEMIND_TITLE", Usage: "Specify a title of the application", Destination: &conf.Title},
cli.StringFlag{Name: "processes, l", EnvVar: "HIVEMIND_PROCESSES", Usage: "Specify process names to launch. Divide names with comma", Destination: &conf.ProcNames},
cli.StringFlag{Name: "title, w", EnvVar: "HIVEMIND_TITLE", Usage: "specify a title of the application", Destination: &conf.Title},
cli.StringFlag{Name: "processes, l", EnvVar: "HIVEMIND_PROCESSES", Usage: "specify process names to launch. Divide names with comma", Destination: &conf.ProcNames},
cli.IntFlag{Name: "port, p", EnvVar: "HIVEMIND_PORT,PORT", Usage: "specify a port to use as the base", Value: 5000, Destination: &conf.PortBase},
cli.IntFlag{Name: "port-step, P", EnvVar: "HIVEMIND_PORT_STEP", Usage: "specify a step to increase port number", Value: 100, Destination: &conf.PortStep},
cli.StringFlag{Name: "root, d", EnvVar: "HIVEMIND_ROOT", Usage: "specify a working directory of application. Default: directory containing the Procfile", Destination: &conf.Root},
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},
cli.BoolFlag{Name: "no-prefix", EnvVar: "HIVEMIND_NO_PREFIX", Usage: "process names will not be printed if the flag is specified", Destination: &conf.NoPrefix},
cli.BoolFlag{Name: "print-timestamps, T", EnvVar: "HIVEMIND_PRINT_TIMESTAMPS", Usage: "timestamps will be printed if the flag is specified", Destination: &conf.PrintTimestamps},
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},
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},
}

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

newHivemind(conf).Run()
exitCode := newHivemind(conf).Run()
if exitCode > 0 && conf.ExitWithHighest {
return cli.NewExitError("At least one process failed", exitCode)
}

return nil
}
Expand Down
4 changes: 3 additions & 1 deletion process.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (p *process) Running() bool {
return p.Process != nil && p.ProcessState == nil
}

func (p *process) Run() {
func (p *process) Run() bool {
p.output.PipeOutput(p)
defer p.output.ClosePipe(p)

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

if err := p.Cmd.Run(); err != nil {
p.writeErr(err)
return false
} else {
p.writeLine([]byte("\033[1mProcess exited\033[0m"))
return true
}
}

Expand Down
23 changes: 23 additions & 0 deletions test-as-job-runner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

echo
echo "As process manager"
dist/hivemind --exit-with-highest-exit-code - <<PROCFILE
job1: echo "job1 is running"; sleep 0.5; exit 13; echo "Done"
job2: echo "job2 is running"; sleep 2; echo "Done"
PROCFILE

echo
echo "As Job Runner, all completing"
dist/hivemind --as-job-runner --exit-with-highest-exit-code - <<PROCFILE
job1: echo "short job1 is running"; sleep 0.5; echo "Done"
job2: echo "short job2 is running"; sleep 1.0; echo "Done"
job3: echo "long job3 is running"; sleep 2; echo "Done"
PROCFILE

echo
echo "As Job Runner, one exits and fail fast"
dist/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 0.5; echo 1; sleep 0.5; echo 2; sleep 0.5; echo 3; sleep 1; echo "Done"
PROCFILE

0 comments on commit c614266

Please sign in to comment.