Last active
June 29, 2019 18:18
-
-
Save vishnevskiy/279b835ea50e44bcb589 to your computer and use it in GitHub Desktop.
Allows running an Elixir/Erlang subprocess which can be gracefully restarted by daisy chaining spawned children.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"flag" | |
"fmt" | |
"io/ioutil" | |
"log" | |
"os" | |
"os/exec" | |
"os/signal" | |
"strings" | |
"sync" | |
"syscall" | |
) | |
var ( | |
// Instances are always increasing to avoid name collisions. | |
index = 0 | |
instances = make(map[int]*exec.Cmd) | |
currentInstance *exec.Cmd | |
// namePerefix and hostname are used to generate Erlang distributed names. | |
namePrefix string | |
hostname string | |
// vm.args file if provded will be generated before starting the process everytime. | |
vmArgsFile string | |
vmArgsTmpl = "-name %s\n-setcookie %s\n+K true\n" | |
// Mutex ensures instances cannot be started concurrently. | |
mutex sync.Mutex | |
) | |
// Kill all running instances and exit with code. | |
func exit(code int) { | |
for _, instance := range instances { | |
instance.Process.Kill() | |
} | |
os.Exit(code) | |
} | |
// Erlang distributed name. | |
func name(index int) string { | |
return fmt.Sprintf("%s%d@%s", namePrefix, index, hostname) | |
} | |
// Start an instance of the executable. | |
func start(executable string, args []string) { | |
mutex.Lock() | |
index += 1 | |
logger := log.New(os.Stdout, fmt.Sprintf("[%s] ", name(index)), 0) | |
erlFlags := fmt.Sprintf(vmArgsTmpl, name(index), namePrefix) | |
// Start instance. | |
cmd := exec.Command(executable, args...) | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
cmd.Env = os.Environ() | |
// If vm.args is provided then set the file, otherwise use environ. | |
if vmArgsFile != "" { | |
ioutil.WriteFile(vmArgsFile, []byte(erlFlags), 0777) | |
} else { | |
// Replace newlines with spaces for environ. | |
erlFlags = strings.Replace(erlFlags, "\n", " ", -1) | |
cmd.Env = append(cmd.Env, "ERL_FLAGS="+erlFlags) | |
} | |
// cmd.Env = append(os.Environ(), "ERL_FLAGS=-name "+name(index)) | |
if currentInstance != nil { | |
// Include previous sibling name for new instance. | |
cmd.Env = append(cmd.Env, "GRACEX_SIBLING="+name(index-1)) | |
} | |
if err := cmd.Start(); err != nil { | |
logger.Fatal(err) | |
} | |
currentInstance = cmd | |
instances[cmd.Process.Pid] = cmd | |
mutex.Unlock() | |
logger.Println("started") | |
if err := cmd.Wait(); err != nil { | |
// If instance failes to start just exist completely and kill | |
// all instances. | |
logger.Println("crashed") | |
exit(1) | |
} else if currentInstance == cmd { | |
// If the current instance exits then no other instances are | |
// running and should just exit. | |
logger.Println("exited") | |
exit(0) | |
} else { | |
// Remove instance from list of running instances once it | |
// has gracefully exited. | |
logger.Println("exited gracefully") | |
delete(instances, cmd.Process.Pid) | |
} | |
} | |
// Processes may receive the `GRACEX_SIBLING` environment variable which contains an Erlang node | |
// and should contact it and tell it to begin gracefully shutting down. The new node should also | |
// proxy over any processes via itself so service is not interutped. | |
// | |
// Examples: | |
// gracex --name a mix run --no-halt | |
// gracex --name a --vmargs rel/test/releases/0.0.1/vm.args rel/test/bin/test foreground | |
func main() { | |
flag.StringVar(&namePrefix, "name", "gracex", "prefix for generated name") | |
flag.StringVar(&hostname, "hostname", "127.0.0.1", "hostname used to generate name") | |
flag.StringVar(&vmArgsFile, "vmargs", "", "path to vm.args file") | |
flag.Parse() | |
// Use remaining args as launch variables. | |
executable, _ := exec.LookPath(flag.Arg(0)) | |
args := flag.Args()[1:] | |
// Start first instance. | |
go start(executable, args) | |
// Listen for SIGHUP and start new instances. | |
c := make(chan os.Signal, 1) | |
signal.Notify(c, syscall.SIGHUP) | |
for s := range c { | |
switch s { | |
case syscall.SIGHUP: | |
go start(executable, args) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment