Skip to content

Commit 8bd9235

Browse files
authored
Merge pull request #40 from snorwin/multi-proc-mgmt
Multi proc mgmt #2
2 parents 75ba865 + 65990bc commit 8bd9235

File tree

4 files changed

+112
-56
lines changed

4 files changed

+112
-56
lines changed

.github/workflows/test.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ jobs:
6666
sleep 90
6767
- name: Verify that configuration was reloaded hitless
6868
run: |
69-
if [ $( kubectl logs haproxy | grep "reload successful" | wc -l) -ne 1 ]; then
69+
if [ $( kubectl logs haproxy | grep "process started with pid" | wc -l) -ne 2 ]; then
70+
exit 1
71+
fi
72+
if [ "$(kubectl logs haproxy | grep -E -c 'process [0-9]+ terminated')" -ne 1 ]; then
7073
exit 1
7174
fi
7275
if [ $(kubectl logs http-client-01 | grep error | wc -l) -gt 0 ]; then
@@ -78,7 +81,7 @@ jobs:
7881
sleep 90
7982
- name: Verify that configuration was NOT reloaded
8083
run: |
81-
if [ $( kubectl logs haproxy | grep "reload failed" | wc -l) -ne 1 ]; then
84+
if [ $( kubectl logs haproxy | grep "validate failed" | wc -l) -ne 1 ]; then
8285
exit 1
8386
fi
8487
if [ $(kubectl logs http-client-01 | grep error | wc -l) -gt 0 ]; then
@@ -90,7 +93,10 @@ jobs:
9093
sleep 90
9194
- name: Verify that configuration was reloaded hitless
9295
run: |
93-
if [ $( kubectl logs haproxy | grep "reload successful" | wc -l) -ne 2 ]; then
96+
if [ $( kubectl logs haproxy | grep "process started with pid" | wc -l) -ne 3 ]; then
97+
exit 1
98+
fi
99+
if [ "$(kubectl logs haproxy | grep -E -c 'process [0-9]+ terminated')" -ne 2 ]; then
94100
exit 1
95101
fi
96102
if [ $(kubectl logs http-client-01 | grep error | wc -l) -gt 0 ]; then

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.24.1-alpine3.21 as builder
1+
FROM golang:1.25.5-alpine3.23 AS builder
22
ARG VERSION
33
ARG HASH
44

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/snorwin/haproxy-reload-wrapper
22

3-
go 1.24.1
3+
go 1.25.5
44

55
require github.com/fsnotify/fsnotify v1.9.0
66

main.go

Lines changed: 101 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"os/signal"
77
"strconv"
8+
"sync"
89
"syscall"
910

1011
"github.com/fsnotify/fsnotify"
@@ -13,24 +14,24 @@ import (
1314
"github.com/snorwin/haproxy-reload-wrapper/pkg/utils"
1415
)
1516

17+
var (
18+
executable string
19+
cmds []*exec.Cmd
20+
l sync.Mutex
21+
terminated bool
22+
)
23+
1624
func main() {
1725
// fetch the absolut path of the haproxy executable
18-
executable, err := utils.LookupExecutablePathAbs("haproxy")
26+
var err error
27+
executable, err = utils.LookupExecutablePathAbs("haproxy")
1928
if err != nil {
2029
log.Emergency(err.Error())
2130
os.Exit(1)
2231
}
2332

24-
// execute haproxy with the flags provided as a child process asynchronously
25-
cmd := exec.Command(executable, os.Args[1:]...)
26-
cmd.Stdout = os.Stdout
27-
cmd.Stderr = os.Stderr
28-
cmd.Env = utils.LoadEnvFile()
29-
if err := cmd.AsyncRun(); err != nil {
30-
log.Emergency(err.Error())
31-
os.Exit(1)
32-
}
33-
log.Notice(fmt.Sprintf("process %d started", cmd.Process.Pid))
33+
// execute haproxy with the flags provided as a child process
34+
runInstance()
3435

3536
watchPath := utils.LookupWatchPath()
3637
if watchPath == "" {
@@ -54,9 +55,6 @@ func main() {
5455
log.Notice(fmt.Sprintf("watch : %s", watchPath))
5556
}
5657

57-
// flag used for termination handling
58-
var terminated bool
59-
6058
// initialize a signal handler for SIGINT, SIGTERM and SIGUSR1 (for OpenShift)
6159
sigs := make(chan os.Signal, 1)
6260
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
@@ -81,62 +79,114 @@ func main() {
8179
}
8280
}
8381

84-
// create a new haproxy process which will replace the old one after it was successfully started
85-
tmp := exec.Command(executable, append([]string{"-x", utils.LookupHAProxySocketPath(), "-sf", strconv.Itoa(cmd.Process.Pid)}, os.Args[1:]...)...)
86-
tmp.Stdout = os.Stdout
87-
tmp.Stderr = os.Stderr
88-
tmp.Env = utils.LoadEnvFile()
89-
90-
if err := tmp.AsyncRun(); err != nil {
91-
log.Warning(err.Error())
92-
log.Warning("reload failed")
93-
continue
94-
}
82+
// create a new haproxy process which will take over listeners
83+
// from the previous ones after it was successfully started
84+
runInstance()
9585

96-
log.Notice(fmt.Sprintf("process %d started", tmp.Process.Pid))
97-
select {
98-
case <-cmd.Terminated:
99-
// old haproxy terminated - successfully started a new process replacing the old one
100-
log.Notice(fmt.Sprintf("process %d terminated : %s", cmd.Process.Pid, cmd.Status()))
101-
log.Notice("reload successful")
102-
cmd = tmp
103-
case <-tmp.Terminated:
104-
// new haproxy terminated without terminating the old process - this can happen if the modified configuration file was invalid
105-
log.Warning(fmt.Sprintf("process %d terminated unexpectedly : %s", tmp.Process.Pid, tmp.Status()))
106-
log.Warning("reload failed")
107-
}
10886
case err := <-fswatch.Errors:
10987
// handle errors of fsnotify.Watcher
11088
log.Alert(err.Error())
11189
case sig := <-sigs:
11290
// handle SIGINT, SIGTERM, SIGUSR1 and propagate it to child process
11391
log.Notice(fmt.Sprintf("received singal %d", sig))
11492

115-
if cmd.Process == nil {
93+
if len(cmds) == 0 {
11694
// received termination suddenly before child process was even started
11795
os.Exit(0)
11896
}
11997

12098
// set termination flag before propagating the signal in order to prevent race conditions
12199
terminated = true
122100

123-
// propagate signal to child process
124-
if err := cmd.Process.Signal(sig); err != nil {
125-
log.Warning(fmt.Sprintf("propagating signal %d to process %d failed", sig, cmd.Process.Pid))
126-
}
127-
case <-cmd.Terminated:
128-
// check for unexpected termination
129-
if !terminated {
130-
log.Emergency(fmt.Sprintf("process %d teminated unexpectedly : %s", cmd.Process.Pid, cmd.Status()))
131-
if cmd.ProcessState != nil && cmd.ProcessState.ExitCode() != 0 {
132-
os.Exit(cmd.ProcessState.ExitCode())
133-
} else {
134-
os.Exit(1)
101+
// propagate signal to child processes
102+
for i := range cmds {
103+
if cmds[i].Process != nil {
104+
if err := cmds[i].Process.Signal(sig); err != nil {
105+
log.Warning(fmt.Sprintf("propagating signal %d to process %d failed", sig, cmds[i].Process.Pid))
106+
}
135107
}
136108
}
109+
}
110+
}
111+
}
112+
113+
func runInstance() {
114+
115+
// validate the config by using the "-c" flag
116+
argsValidate := append(os.Args[1:], "-c")
117+
cmdValidate := exec.Command(executable, argsValidate...)
118+
cmdValidate.Stdout = os.Stdout
119+
cmdValidate.Stderr = os.Stderr
120+
cmdValidate.Env = utils.LoadEnvFile()
121+
122+
if err := cmdValidate.Run(); err != nil {
123+
log.Warning("validate failed: " + err.Error())
124+
// exit if the config is invalid and no other process is running
125+
if len(cmds) == 0 {
126+
os.Exit(1)
127+
}
128+
return
129+
}
130+
131+
// launch the actual haproxy including the previous pids to terminate
132+
args := os.Args[1:]
133+
if len(cmds) > 0 {
134+
args = append(args, []string{"-x", utils.LookupHAProxySocketPath(), "-sf", pids()}...)
135+
}
136+
137+
cmd := exec.Command(executable, args...)
138+
cmd.Stdout = os.Stdout
139+
cmd.Stderr = os.Stderr
140+
cmd.Env = utils.LoadEnvFile()
141+
142+
if err := cmd.AsyncRun(); err != nil {
143+
log.Warning("process starting failed: " + err.Error())
144+
}
145+
go func(cmd *exec.Cmd) {
146+
<-cmd.Terminated
147+
log.Notice(fmt.Sprintf("process %d terminated : %s", cmd.Process.Pid, cmd.Status()))
137148

138-
log.Notice(fmt.Sprintf("process %d terminated : %s", cmd.Process.Pid, cmd.Status()))
149+
// exit if termination signal was received and the last process terminated abnormally
150+
if terminated && cmd.ProcessState.ExitCode() != 0 {
139151
os.Exit(cmd.ProcessState.ExitCode())
140152
}
153+
154+
// remove the process from tracking
155+
l.Lock()
156+
defer l.Unlock()
157+
for i := range cmds {
158+
if cmds[i].Process.Pid == cmd.Process.Pid {
159+
cmds = append(cmds[:i], cmds[i+1:]...)
160+
break
161+
}
162+
}
163+
164+
// exit if there are no more processes running
165+
if len(cmds) == 0 {
166+
if cmd.ProcessState != nil && cmd.ProcessState.ExitCode() != 0 {
167+
os.Exit(cmd.ProcessState.ExitCode())
168+
} else {
169+
os.Exit(0)
170+
}
171+
}
172+
}(cmd)
173+
174+
log.Notice(fmt.Sprintf("process started with pid %d and status %s", cmd.Process.Pid, cmd.Status()))
175+
176+
l.Lock()
177+
defer l.Unlock()
178+
cmds = append(cmds, cmd)
179+
}
180+
181+
func pids() string {
182+
var str string
183+
if len(cmds) == 0 {
184+
return str
141185
}
186+
187+
for i := range cmds {
188+
str = strconv.Itoa(cmds[i].Process.Pid) + " " + str
189+
}
190+
191+
return str
142192
}

0 commit comments

Comments
 (0)