Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
Peter Bourgon | b4b2c78530 | |
Peter Bourgon | 9ae8b38549 | |
Peter Bourgon | 9f0d0ba5a7 | |
Peter Bourgon | 3c8f911b51 | |
Peter Bourgon | a02bfa75fa | |
Peter Bourgon | f197aa8f6a | |
Peter Bourgon | 819c47186c | |
Peter Bourgon | 05fe1d1fa6 |
|
@ -1,6 +1,9 @@
|
||||||
runsvinit
|
runsvinit
|
||||||
runsvinit-*-*
|
runsvinit-*-*
|
||||||
examples/runsvinit-linux-amd64*
|
examples/runsvinit-linux-amd64*
|
||||||
|
zombietest/zombie
|
||||||
|
zombietest/runsvinit
|
||||||
|
*.uptodate
|
||||||
|
|
||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
*.o
|
*.o
|
||||||
|
|
34
README.md
34
README.md
|
@ -1,19 +1,27 @@
|
||||||
# runsvinit
|
# runsvinit [![Circle CI](https://circleci.com/gh/peterbourgon/runsvinit.svg?style=svg)](https://circleci.com/gh/peterbourgon/runsvinit)
|
||||||
|
|
||||||
If you have a Docker container that's a collection of runit-supervised daemons,
|
If you have a Docker container that's a collection of runit-supervised daemons, this process is suitable for use as the ENTRYPOINT.
|
||||||
this process is suitable for use as the ENTRYPOINT.
|
|
||||||
See [the example](https://github.com/peterbourgon/runsvinit/tree/master/example).
|
See [the example](https://github.com/peterbourgon/runsvinit/tree/master/example).
|
||||||
|
|
||||||
**Why not just exec runsvdir?**
|
**Why not use runit(8) directly?**
|
||||||
|
|
||||||
`docker stop` issues SIGTERM (or, in a future version of Docker, perhaps another custom signal)
|
[runit(8)](http://smarden.org/runit/runit.8.html) is designed to be used as process 1.
|
||||||
but if runsvdir receives a signal,
|
And, if you provide an `/etc/service/ctrlaltdel` script, it will be executed when runit receives the INT signal.
|
||||||
it doesn't wait for its supervised processes to exit before returning.
|
So, we could use that hook to gracefully terminate our services.
|
||||||
If you don't care about graceful shutdown of your daemons, no problem, you don't need this tool.
|
But Docker only sends TERM on `docker stop`.
|
||||||
|
|
||||||
**Why not wrap runsvdir in a simple shell script?**
|
Note that the container stop signal [will become configurable](https://github.com/docker/docker/pull/15307) in Docker 1.9.
|
||||||
|
Once Docker 1.9 ships, this utility will be obsolete.
|
||||||
|
|
||||||
This works great:
|
**Why not just exec runsvdir(8) directly?**
|
||||||
|
|
||||||
|
If [runsvdir(8)](http://smarden.org/runit/runsvdir.8.html) receives a signal, it doesn't wait for its supervised processes to exit before returning.
|
||||||
|
|
||||||
|
**Why not wrap runsvdir(8) in a simple shell script?**
|
||||||
|
|
||||||
|
Process 1 has the additional responsibility of [reaping orphaned child processes](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/).
|
||||||
|
To the best of my knowledge, there is no sane way to do this with a shell script.
|
||||||
|
Otherwise, indeed, this would work great:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
@ -30,14 +38,10 @@ trap "sv_stop; exit" SIGTERM
|
||||||
wait
|
wait
|
||||||
```
|
```
|
||||||
|
|
||||||
...except it doesn't [reap orphaned child processes](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/)
|
|
||||||
and is therefore unsuitable for being PID 1.
|
|
||||||
|
|
||||||
**Why not use my_init from [phusion/baseimage-docker](https://github.com/phusion/baseimage-docker)?**
|
**Why not use my_init from [phusion/baseimage-docker](https://github.com/phusion/baseimage-docker)?**
|
||||||
|
|
||||||
That works great — if you're willing to add python3 to your Docker images :)
|
my_init depends on Python 3, which might be a big dependency for such a small responsibility.
|
||||||
|
|
||||||
**So this is just a stripped-down my_init in Go?**
|
**So this is just a stripped-down my_init in Go?**
|
||||||
|
|
||||||
Basically, yes.
|
Basically, yes.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
machine:
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
test:
|
||||||
|
override:
|
||||||
|
- go test -v
|
||||||
|
- make CIRCLECI=true SUDO=sudo RMCONTAINER= -C zombietest
|
|
@ -2,9 +2,9 @@
|
||||||
docker: runsvinit
|
docker: runsvinit
|
||||||
docker build -t runsvinit-example .
|
docker build -t runsvinit-example .
|
||||||
|
|
||||||
runsvinit: runsvinit-linux-amd64.tgz
|
runsvinit: $GOPATH/bin/runsvinit
|
||||||
tar zxf $<
|
cp $< $@
|
||||||
|
|
||||||
runsvinit-linux-amd64.tgz:
|
$GOPATH/bin/runsvinit:
|
||||||
wget --quiet https://github.com/peterbourgon/runsvinit/releases/download/v2.0.0/runsvinit-linux-amd64.tgz
|
go get -u github.com/peterbourgon/runsvinit
|
||||||
|
|
||||||
|
|
97
main.go
97
main.go
|
@ -1,68 +1,93 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
const etcService = "/etc/service"
|
const etcService = "/etc/service"
|
||||||
|
|
||||||
|
var (
|
||||||
|
debugf = log.Printf
|
||||||
|
info = log.Print
|
||||||
|
infof = log.Printf
|
||||||
|
fatal = log.Fatal
|
||||||
|
fatalf = log.Fatalf
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
var (
|
||||||
|
reap = flag.Bool("reap", true, "reap orphan children")
|
||||||
|
debug = flag.Bool("debug", false, "log debug information")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
if !*debug {
|
||||||
|
debugf = func(string, ...interface{}) {}
|
||||||
|
}
|
||||||
|
|
||||||
runsvdir, err := exec.LookPath("runsvdir")
|
runsvdir, err := exec.LookPath("runsvdir")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sv, err := exec.LookPath("sv")
|
sv, err := exec.LookPath("sv")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fi, err := os.Stat(etcService); err != nil {
|
if fi, err := os.Stat(etcService); err != nil {
|
||||||
log.Fatal(err)
|
fatal(err)
|
||||||
} else if !fi.IsDir() {
|
} else if !fi.IsDir() {
|
||||||
log.Fatalf("%s is not a directory", etcService)
|
fatalf("%s is not a directory", etcService)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pid := os.Getpid(); pid != 1 {
|
if pid := os.Getpid(); pid != 1 {
|
||||||
log.Printf("warning: I'm not PID 1, I'm PID %d", pid)
|
debugf("warning: I'm not PID 1, I'm PID %d", pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
go reapAll()
|
if *reap {
|
||||||
|
go reapLoop()
|
||||||
|
} else {
|
||||||
|
infof("warning: NOT reaping zombies")
|
||||||
|
}
|
||||||
|
|
||||||
supervisor := cmd(runsvdir, etcService)
|
supervisor := cmd(runsvdir, etcService)
|
||||||
if err := supervisor.Start(); err != nil {
|
if err := supervisor.Start(); err != nil {
|
||||||
log.Fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("%s started", runsvdir)
|
debugf("%s started", runsvdir)
|
||||||
|
|
||||||
go shutdown(sv, supervisor.Process)
|
go shutdown(sv, supervisor.Process)
|
||||||
|
|
||||||
if err := supervisor.Wait(); err != nil {
|
if err := supervisor.Wait(); err != nil {
|
||||||
log.Printf("%s exited with error: %v", runsvdir, err)
|
infof("%s exited with error: %v", runsvdir, err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("%s exited cleanly", runsvdir)
|
debugf("%s exited cleanly", runsvdir)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func reapAll() {
|
|
||||||
c := make(chan os.Signal)
|
|
||||||
signal.Notify(c, syscall.SIGCHLD)
|
|
||||||
for range c {
|
|
||||||
go reapOne()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// From https://github.com/ramr/go-reaper/blob/master/reaper.go
|
// From https://github.com/ramr/go-reaper/blob/master/reaper.go
|
||||||
func reapOne() {
|
func reapLoop() {
|
||||||
|
c := make(chan os.Signal)
|
||||||
|
signal.Notify(c, syscall.SIGCHLD)
|
||||||
|
for range c {
|
||||||
|
reapChildren()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reapChildren() {
|
||||||
|
for {
|
||||||
var (
|
var (
|
||||||
ws syscall.WaitStatus
|
ws syscall.WaitStatus
|
||||||
pid int
|
pid int
|
||||||
|
@ -75,9 +100,10 @@ func reapOne() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == syscall.ECHILD {
|
if err == syscall.ECHILD {
|
||||||
return
|
return // done
|
||||||
|
}
|
||||||
|
infof("reaped child process %d (%+v)", pid, ws)
|
||||||
}
|
}
|
||||||
log.Printf("reaped child process %d (%+v)", pid, ws)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type signaler interface {
|
type signaler interface {
|
||||||
|
@ -88,40 +114,45 @@ func shutdown(sv string, s signaler) {
|
||||||
c := make(chan os.Signal)
|
c := make(chan os.Signal)
|
||||||
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
|
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
|
||||||
sig := <-c
|
sig := <-c
|
||||||
log.Printf("received %s", sig)
|
debugf("received %s", sig)
|
||||||
|
|
||||||
matches, err := filepath.Glob(filepath.Join(etcService, "*"))
|
matches, err := filepath.Glob(filepath.Join(etcService, "*"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("when shutting down services: %v", err)
|
infof("when shutting down services: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var stopped []string
|
var wg sync.WaitGroup
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
fi, err := os.Stat(match)
|
fi, err := os.Stat(match)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%s: %v", match, err)
|
infof("%s: %v", match, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !fi.IsDir() {
|
if !fi.IsDir() {
|
||||||
log.Printf("%s: not a directory", match)
|
infof("%s: not a directory", match)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
service := filepath.Base(match)
|
service := filepath.Base(match)
|
||||||
stop := cmd(sv, "stop", service)
|
stop := cmd(sv, "stop", service)
|
||||||
|
debugf("stopping %s...", service)
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
if err := stop.Run(); err != nil {
|
if err := stop.Run(); err != nil {
|
||||||
log.Printf("%s: %v", strings.Join(stop.Args, " "), err)
|
infof("%s: %v", strings.Join(stop.Args, " "), err)
|
||||||
continue
|
} else {
|
||||||
|
debugf("stopped %s", service)
|
||||||
}
|
}
|
||||||
stopped = append(stopped, service)
|
}()
|
||||||
}
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
log.Printf("stopped %d: %s", len(stopped), strings.Join(stopped, ", "))
|
debugf("stopping supervisor with signal %s...", sig)
|
||||||
log.Printf("stopping supervisor with signal %s...", sig)
|
|
||||||
if err := s.Signal(sig); err != nil {
|
if err := s.Signal(sig); err != nil {
|
||||||
log.Print(err)
|
info(err)
|
||||||
}
|
}
|
||||||
log.Printf("shutdown handler exiting")
|
debugf("shutdown handler exiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmd(path string, args ...string) *exec.Cmd {
|
func cmd(path string, args ...string) *exec.Cmd {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN echo "http://dl-4.alpinelinux.org/alpine/edge/testing" >>/etc/apk/repositories && \
|
||||||
|
apk add --update runit && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
COPY zombie /
|
||||||
|
RUN mkdir -p /etc/service/zombie
|
||||||
|
COPY run-zombie /etc/service/zombie/run
|
||||||
|
|
||||||
|
COPY /runsvinit /
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
GO ?= go
|
||||||
|
SUDO ?=
|
||||||
|
RMCONTAINER ?= --rm
|
||||||
|
|
||||||
|
.PHONY: test clean
|
||||||
|
|
||||||
|
test: .test.uptodate
|
||||||
|
./test.bash
|
||||||
|
|
||||||
|
.test.uptodate: runsvinit zombie run-zombie Dockerfile
|
||||||
|
$(SUDO) docker build -t zombietest .
|
||||||
|
touch $@
|
||||||
|
|
||||||
|
runsvinit: ../*.go
|
||||||
|
env GOOS=linux GOARCH=amd64 $(GO) build -o $@ github.com/peterbourgon/runsvinit
|
||||||
|
|
||||||
|
zombie: .build.uptodate
|
||||||
|
$(SUDO) docker run $(RMCONTAINER) -v $(shell pwd):/mount zombietest-build cc -Wall -Werror -o /mount/zombie /zombie.c
|
||||||
|
|
||||||
|
.build.uptodate: build/zombie.c build/Dockerfile
|
||||||
|
$(SUDO) docker build -t zombietest-build build/
|
||||||
|
touch $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .test.uptodate .build.uptodate runsvinit zombie
|
||||||
|
$(SUDO) docker stop zombietest zombietest-build >/dev/null 2>&1 || true
|
||||||
|
$(SUDO) docker rm zombietest zombietest-build >/dev/null 2>&1 || true
|
|
@ -0,0 +1,24 @@
|
||||||
|
# zombietest
|
||||||
|
|
||||||
|
This directory contains an integration test to prove runsvinit is actually
|
||||||
|
reaping zombies. `make` builds and executes the test as follows:
|
||||||
|
|
||||||
|
1. We produce a linux/amd64 runsvinit binary by setting GOOS/GOARCH and
|
||||||
|
invoking the Go compiler. Requires Go 1.5, or Go 1.4 built with the
|
||||||
|
appropriate cross-compile options.
|
||||||
|
|
||||||
|
2. The build/zombie.c program spawns five zombies and exits. We compile it for
|
||||||
|
linux/amd64 via a zombietest-build container. We do this so `make` works
|
||||||
|
from a Mac. This requires a working Docker installation.
|
||||||
|
|
||||||
|
3. Once we have linux/amd64 runsvinit and zombie binaries, we produce a
|
||||||
|
zombietest container via the Dockerfile. That container contains a single
|
||||||
|
runit service, /etc/service/zombie, which supervises the zombie binary. We
|
||||||
|
provide no default ENTRYPOINT, so we can supply it at runtime.
|
||||||
|
|
||||||
|
4. Once the zombietest container is built, we invoke the test.bash script.
|
||||||
|
That launches a version of the container with runsvinit set to NOT reap
|
||||||
|
zombies, and after 1 second, verifies that zombies exist. Then, it launches
|
||||||
|
a version of the container with runsvinit set to reap zombies, and after 1
|
||||||
|
second, verifies that no zombies exist.
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk add --update gcc musl-dev && rm -rf /var/cache/apk/*
|
||||||
|
COPY zombie.c /
|
|
@ -0,0 +1,18 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pid_t pid;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i<5; i++) {
|
||||||
|
pid = fork();
|
||||||
|
if (pid > 0) {
|
||||||
|
printf("Zombie #%d born\n", i + 1);
|
||||||
|
} else {
|
||||||
|
printf("Brains...\n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
exec /zombie
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function zombies() {
|
||||||
|
if [ -z "$CIRCLECI" ]
|
||||||
|
then
|
||||||
|
docker exec $C ps -o pid,stat | grep Z | wc -l
|
||||||
|
else
|
||||||
|
# https://circleci.com/docs/docker#docker-exec
|
||||||
|
sudo lxc-attach -n "$(docker inspect --format '{{.Id}}' $C)" -- sh -c "ps -o pid,stat | grep Z | wc -l"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop_rm() {
|
||||||
|
docker stop $1 >/dev/null
|
||||||
|
#docker logs $1
|
||||||
|
docker rm $1 >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
SLEEP=1
|
||||||
|
RC=0
|
||||||
|
|
||||||
|
C=$(docker run -d zombietest /runsvinit -reap=false -debug)
|
||||||
|
sleep $SLEEP
|
||||||
|
NOREAP=$(zombies)
|
||||||
|
echo -n without reaping, we have $NOREAP zombies...
|
||||||
|
if [ "$NOREAP" -le "0" ]
|
||||||
|
then
|
||||||
|
echo " FAIL"
|
||||||
|
RC=1
|
||||||
|
else
|
||||||
|
echo " good"
|
||||||
|
fi
|
||||||
|
stop_rm $C
|
||||||
|
|
||||||
|
C=$(docker run -d zombietest /runsvinit -debug)
|
||||||
|
sleep $SLEEP
|
||||||
|
YESREAP=$(zombies)
|
||||||
|
echo -n with reaping, we have $YESREAP zombies...
|
||||||
|
if [ "$YESREAP" -gt "0" ]
|
||||||
|
then
|
||||||
|
echo " FAIL"
|
||||||
|
RC=1
|
||||||
|
else
|
||||||
|
echo " good"
|
||||||
|
fi
|
||||||
|
stop_rm $C
|
||||||
|
|
||||||
|
exit $RC
|
Loading…
Reference in New Issue