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-*-*
|
||||
examples/runsvinit-linux-amd64*
|
||||
zombietest/zombie
|
||||
zombietest/runsvinit
|
||||
*.uptodate
|
||||
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.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,
|
||||
this process is suitable for use as the ENTRYPOINT.
|
||||
If you have a Docker container that's a collection of runit-supervised daemons, this process is suitable for use as the ENTRYPOINT.
|
||||
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)
|
||||
but if runsvdir receives a signal,
|
||||
it doesn't wait for its supervised processes to exit before returning.
|
||||
If you don't care about graceful shutdown of your daemons, no problem, you don't need this tool.
|
||||
[runit(8)](http://smarden.org/runit/runit.8.html) is designed to be used as process 1.
|
||||
And, if you provide an `/etc/service/ctrlaltdel` script, it will be executed when runit receives the INT signal.
|
||||
So, we could use that hook to gracefully terminate our services.
|
||||
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
|
||||
#!/bin/sh
|
||||
|
@ -30,14 +38,10 @@ trap "sv_stop; exit" SIGTERM
|
|||
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)?**
|
||||
|
||||
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?**
|
||||
|
||||
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 build -t runsvinit-example .
|
||||
|
||||
runsvinit: runsvinit-linux-amd64.tgz
|
||||
tar zxf $<
|
||||
runsvinit: $GOPATH/bin/runsvinit
|
||||
cp $< $@
|
||||
|
||||
runsvinit-linux-amd64.tgz:
|
||||
wget --quiet https://github.com/peterbourgon/runsvinit/releases/download/v2.0.0/runsvinit-linux-amd64.tgz
|
||||
$GOPATH/bin/runsvinit:
|
||||
go get -u github.com/peterbourgon/runsvinit
|
||||
|
||||
|
|
121
main.go
121
main.go
|
@ -1,83 +1,109 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const etcService = "/etc/service"
|
||||
|
||||
var (
|
||||
debugf = log.Printf
|
||||
info = log.Print
|
||||
infof = log.Printf
|
||||
fatal = log.Fatal
|
||||
fatalf = log.Fatalf
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
reap = flag.Bool("reap", true, "reap orphan children")
|
||||
debug = flag.Bool("debug", false, "log debug information")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
log.SetFlags(0)
|
||||
|
||||
if !*debug {
|
||||
debugf = func(string, ...interface{}) {}
|
||||
}
|
||||
|
||||
runsvdir, err := exec.LookPath("runsvdir")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
sv, err := exec.LookPath("sv")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(etcService); err != nil {
|
||||
log.Fatal(err)
|
||||
fatal(err)
|
||||
} 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 {
|
||||
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)
|
||||
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)
|
||||
|
||||
if err := supervisor.Wait(); err != nil {
|
||||
log.Printf("%s exited with error: %v", runsvdir, err)
|
||||
infof("%s exited with error: %v", runsvdir, err)
|
||||
} else {
|
||||
log.Printf("%s exited cleanly", runsvdir)
|
||||
}
|
||||
}
|
||||
|
||||
func reapAll() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGCHLD)
|
||||
for range c {
|
||||
go reapOne()
|
||||
debugf("%s exited cleanly", runsvdir)
|
||||
}
|
||||
}
|
||||
|
||||
// From https://github.com/ramr/go-reaper/blob/master/reaper.go
|
||||
func reapOne() {
|
||||
var (
|
||||
ws syscall.WaitStatus
|
||||
pid int
|
||||
err error
|
||||
)
|
||||
func reapLoop() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGCHLD)
|
||||
for range c {
|
||||
reapChildren()
|
||||
}
|
||||
}
|
||||
|
||||
func reapChildren() {
|
||||
for {
|
||||
pid, err = syscall.Wait4(-1, &ws, 0, nil)
|
||||
if err != syscall.EINTR {
|
||||
break
|
||||
var (
|
||||
ws syscall.WaitStatus
|
||||
pid int
|
||||
err error
|
||||
)
|
||||
for {
|
||||
pid, err = syscall.Wait4(-1, &ws, 0, nil)
|
||||
if err != syscall.EINTR {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err == syscall.ECHILD {
|
||||
return // done
|
||||
}
|
||||
infof("reaped child process %d (%+v)", pid, ws)
|
||||
}
|
||||
if err == syscall.ECHILD {
|
||||
return
|
||||
}
|
||||
log.Printf("reaped child process %d (%+v)", pid, ws)
|
||||
}
|
||||
|
||||
type signaler interface {
|
||||
|
@ -88,40 +114,45 @@ func shutdown(sv string, s signaler) {
|
|||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
|
||||
sig := <-c
|
||||
log.Printf("received %s", sig)
|
||||
debugf("received %s", sig)
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(etcService, "*"))
|
||||
if err != nil {
|
||||
log.Printf("when shutting down services: %v", err)
|
||||
infof("when shutting down services: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var stopped []string
|
||||
var wg sync.WaitGroup
|
||||
for _, match := range matches {
|
||||
fi, err := os.Stat(match)
|
||||
if err != nil {
|
||||
log.Printf("%s: %v", match, err)
|
||||
infof("%s: %v", match, err)
|
||||
continue
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
log.Printf("%s: not a directory", match)
|
||||
infof("%s: not a directory", match)
|
||||
continue
|
||||
}
|
||||
service := filepath.Base(match)
|
||||
stop := cmd(sv, "stop", service)
|
||||
if err := stop.Run(); err != nil {
|
||||
log.Printf("%s: %v", strings.Join(stop.Args, " "), err)
|
||||
continue
|
||||
}
|
||||
stopped = append(stopped, service)
|
||||
debugf("stopping %s...", service)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := stop.Run(); err != nil {
|
||||
infof("%s: %v", strings.Join(stop.Args, " "), err)
|
||||
} else {
|
||||
debugf("stopped %s", service)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
log.Printf("stopped %d: %s", len(stopped), strings.Join(stopped, ", "))
|
||||
log.Printf("stopping supervisor with signal %s...", sig)
|
||||
debugf("stopping supervisor with signal %s...", sig)
|
||||
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 {
|
||||
|
|
|
@ -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