Skip to content

Instantly share code, notes, and snippets.

@vishnevskiy
Last active June 29, 2019 18:18
Show Gist options
  • Save vishnevskiy/279b835ea50e44bcb589 to your computer and use it in GitHub Desktop.
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.
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