Feat: Env variables to set health timeouts

- HEALTH_OPENVPN_DURATION_INITIAL
- HEALTH_OPENVPN_DURATION_ADDITION
This commit is contained in:
Quentin McGaw (desktop)
2021-07-22 20:13:20 +00:00
parent 8beff34cca
commit 6acb7caf5b
11 changed files with 307 additions and 20 deletions

View File

@@ -110,6 +110,9 @@ ENV VPNSP=pia \
# Openvpn # Openvpn
OPENVPN_CIPHER= \ OPENVPN_CIPHER= \
OPENVPN_AUTH= \ OPENVPN_AUTH= \
# Health
HEALTH_OPENVPN_DURATION_INITIAL=6s \
HEALTH_OPENVPN_DURATION_ADDITION=5s \
# DNS over TLS # DNS over TLS
DOT=on \ DOT=on \
DOT_PROVIDERS=cloudflare \ DOT_PROVIDERS=cloudflare \

View File

@@ -365,8 +365,8 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
controlGroupHandler.Add(httpServerHandler) controlGroupHandler.Add(httpServerHandler)
healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "}) healthLogger := logger.NewChild(logging.Settings{Prefix: "healthcheck: "})
healthcheckServer := healthcheck.NewServer( healthcheckServer := healthcheck.NewServer(constants.HealthcheckAddress,
constants.HealthcheckAddress, healthLogger, openvpnLooper) allSettings.Health, healthLogger, openvpnLooper)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler( healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"HTTP health server", defaultGoRoutineSettings) "HTTP health server", defaultGoRoutineSettings)
go healthcheckServer.Run(healthServerCtx, healthServerDone) go healthcheckServer.Run(healthServerCtx, healthServerDone)

View File

@@ -0,0 +1,41 @@
package configuration
import (
"strings"
"github.com/qdm12/golibs/params"
)
// Health contains settings for the healthcheck and health server.
type Health struct {
OpenVPN HealthyWait
}
func (settings *Health) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *Health) lines() (lines []string) {
lines = append(lines, lastIndent+"Health:")
lines = append(lines, indent+lastIndent+"OpenVPN:")
for _, line := range settings.OpenVPN.lines() {
lines = append(lines, indent+indent+line)
}
return lines
}
func (settings *Health) read(r reader) (err error) {
settings.OpenVPN.Initial, err = r.env.Duration("HEALTH_OPENVPN_DURATION_INITIAL", params.Default("6s"))
if err != nil {
return err
}
settings.OpenVPN.Addition, err = r.env.Duration("HEALTH_OPENVPN_DURATION_ADDITION", params.Default("5s"))
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,150 @@
package configuration
import (
"errors"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Health_String(t *testing.T) {
t.Parallel()
var health Health
const expected = "|--Health:\n |--OpenVPN:\n |--Initial duration: 0s"
s := health.String()
assert.Equal(t, expected, s)
}
func Test_Health_lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings Health
lines []string
}{
"empty": {
lines: []string{
"|--Health:",
" |--OpenVPN:",
" |--Initial duration: 0s",
},
},
"filled settings": {
settings: Health{
OpenVPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
lines: []string{
"|--Health:",
" |--OpenVPN:",
" |--Initial duration: 1s",
" |--Addition duration: 1m0s",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}
func Test_Health_read(t *testing.T) {
t.Parallel()
errDummy := errors.New("dummy")
testCases := map[string]struct {
openvpnInitialDuration time.Duration
openvpnInitialErr error
openvpnAdditionDuration time.Duration
openvpnAdditionErr error
expected Health
err error
}{
"success": {
openvpnInitialDuration: time.Second,
openvpnAdditionDuration: time.Minute,
expected: Health{
OpenVPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
},
"initial error": {
openvpnInitialDuration: time.Second,
openvpnInitialErr: errDummy,
openvpnAdditionDuration: time.Minute,
expected: Health{
OpenVPN: HealthyWait{
Initial: time.Second,
},
},
err: errDummy,
},
"addition error": {
openvpnInitialDuration: time.Second,
openvpnAdditionDuration: time.Minute,
openvpnAdditionErr: errDummy,
expected: Health{
OpenVPN: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
},
err: errDummy,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockEnv(ctrl)
env.EXPECT().
Duration("HEALTH_OPENVPN_DURATION_INITIAL", gomock.Any()).
Return(testCase.openvpnInitialDuration, testCase.openvpnInitialErr)
if testCase.openvpnInitialErr == nil {
env.EXPECT().
Duration("HEALTH_OPENVPN_DURATION_ADDITION", gomock.Any()).
Return(testCase.openvpnAdditionDuration, testCase.openvpnAdditionErr)
}
r := reader{
env: env,
}
var health Health
err := health.read(r)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.expected, health)
})
}
}

View File

@@ -0,0 +1,55 @@
package configuration
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_HealthyWait_String(t *testing.T) {
t.Parallel()
var healthyWait HealthyWait
const expected = "|--Initial duration: 0s"
s := healthyWait.String()
assert.Equal(t, expected, s)
}
func Test_HealthyWait_lines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings HealthyWait
lines []string
}{
"empty": {
lines: []string{
"|--Initial duration: 0s",
},
},
"filled settings": {
settings: HealthyWait{
Initial: time.Second,
Addition: time.Minute,
},
lines: []string{
"|--Initial duration: 1s",
"|--Addition duration: 1m0s",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.lines()
assert.Equal(t, testCase.lines, lines)
})
}
}

View File

@@ -0,0 +1,30 @@
package configuration
import (
"strings"
"time"
)
type HealthyWait struct {
// Initial is the initial duration to wait for the program
// to be healthy before taking action.
Initial time.Duration
// Addition is the duration to add to the Initial duration
// after Initial has expired to wait longer for the program
// to be healthy.
Addition time.Duration
}
func (settings *HealthyWait) String() string {
return strings.Join(settings.lines(), "\n")
}
func (settings *HealthyWait) lines() (lines []string) {
lines = append(lines, lastIndent+"Initial duration: "+settings.Initial.String())
if settings.Addition > 0 {
lines = append(lines, lastIndent+"Addition duration: "+settings.Addition.String())
}
return lines
}

View File

@@ -22,6 +22,7 @@ type Settings struct {
PublicIP PublicIP PublicIP PublicIP
VersionInformation bool VersionInformation bool
ControlServer ControlServer ControlServer ControlServer
Health Health
} }
func (settings *Settings) String() string { func (settings *Settings) String() string {
@@ -36,6 +37,7 @@ func (settings *Settings) lines() (lines []string) {
lines = append(lines, settings.System.lines()...) lines = append(lines, settings.System.lines()...)
lines = append(lines, settings.HTTPProxy.lines()...) lines = append(lines, settings.HTTPProxy.lines()...)
lines = append(lines, settings.ShadowSocks.lines()...) lines = append(lines, settings.ShadowSocks.lines()...)
lines = append(lines, settings.Health.lines()...)
lines = append(lines, settings.ControlServer.lines()...) lines = append(lines, settings.ControlServer.lines()...)
lines = append(lines, settings.Updater.lines()...) lines = append(lines, settings.Updater.lines()...)
lines = append(lines, settings.PublicIP.lines()...) lines = append(lines, settings.PublicIP.lines()...)
@@ -55,6 +57,7 @@ var (
ErrControlServer = errors.New("cannot read control server settings") ErrControlServer = errors.New("cannot read control server settings")
ErrUpdater = errors.New("cannot read Updater settings") ErrUpdater = errors.New("cannot read Updater settings")
ErrPublicIP = errors.New("cannot read Public IP getter settings") ErrPublicIP = errors.New("cannot read Public IP getter settings")
ErrHealth = errors.New("cannot read health settings")
) )
// Read obtains all configuration options for the program and returns an error as soon // Read obtains all configuration options for the program and returns an error as soon
@@ -107,5 +110,9 @@ func (settings *Settings) Read(env params.Env, os os.OS, logger logging.Logger)
return fmt.Errorf("%w: %s", ErrPublicIP, err) return fmt.Errorf("%w: %s", ErrPublicIP, err)
} }
if err := settings.Health.read(r); err != nil {
return fmt.Errorf("%w: %s", ErrHealth, err)
}
return nil return nil
} }

View File

@@ -37,6 +37,9 @@ func Test_Settings_lines(t *testing.T) {
" |--Process user ID: 0", " |--Process user ID: 0",
" |--Process group ID: 0", " |--Process group ID: 0",
" |--Timezone: NOT SET ⚠️ - it can cause time related issues", " |--Timezone: NOT SET ⚠️ - it can cause time related issues",
"|--Health:",
" |--OpenVPN:",
" |--Initial duration: 0s",
"|--HTTP control server:", "|--HTTP control server:",
" |--Listening port: 0", " |--Listening port: 0",
"|--Public IP getter: disabled", "|--Public IP getter: disabled",

View File

@@ -12,7 +12,7 @@ import (
func (s *server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) { func (s *server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) {
defer close(done) defer close(done)
s.openvpn.healthyTimer = time.NewTimer(defaultOpenvpnHealthyWaitTime) s.openvpn.healthyTimer = time.NewTimer(s.openvpn.currentHealthyWait)
for { for {
previousErr := s.handler.getErr() previousErr := s.handler.getErr()
@@ -23,10 +23,10 @@ func (s *server) runHealthcheckLoop(ctx context.Context, done chan<- struct{}) {
if previousErr != nil && err == nil { if previousErr != nil && err == nil {
s.logger.Info("healthy!") s.logger.Info("healthy!")
s.openvpn.healthyTimer.Stop() s.openvpn.healthyTimer.Stop()
s.openvpn.healthyWaitTime = defaultOpenvpnHealthyWaitTime s.openvpn.currentHealthyWait = s.openvpn.healthyWaitConfig.Initial
} else if previousErr == nil && err != nil { } else if previousErr == nil && err != nil {
s.logger.Info("unhealthy: " + err.Error()) s.logger.Info("unhealthy: " + err.Error())
s.openvpn.healthyTimer = time.NewTimer(s.openvpn.healthyWaitTime) s.openvpn.healthyTimer = time.NewTimer(s.openvpn.currentHealthyWait)
} }
if err != nil { // try again after 1 second if err != nil { // try again after 1 second

View File

@@ -9,9 +9,9 @@ import (
func (s *server) onUnhealthyOpenvpn(ctx context.Context) { func (s *server) onUnhealthyOpenvpn(ctx context.Context) {
s.logger.Info("program has been unhealthy for " + s.logger.Info("program has been unhealthy for " +
s.openvpn.healthyWaitTime.String() + ": restarting OpenVPN") s.openvpn.currentHealthyWait.String() + ": restarting OpenVPN")
_, _ = s.openvpn.looper.ApplyStatus(ctx, constants.Stopped) _, _ = s.openvpn.looper.ApplyStatus(ctx, constants.Stopped)
_, _ = s.openvpn.looper.ApplyStatus(ctx, constants.Running) _, _ = s.openvpn.looper.ApplyStatus(ctx, constants.Running)
s.openvpn.healthyWaitTime += openvpnHealthyWaitTimeAdd s.openvpn.currentHealthyWait += s.openvpn.healthyWaitConfig.Addition
s.openvpn.healthyTimer = time.NewTimer(s.openvpn.healthyWaitTime) s.openvpn.healthyTimer = time.NewTimer(s.openvpn.currentHealthyWait)
} }

View File

@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
) )
@@ -25,17 +26,13 @@ type server struct {
type openvpnHealth struct { type openvpnHealth struct {
looper openvpn.Looper looper openvpn.Looper
healthyWaitTime time.Duration healthyWaitConfig configuration.HealthyWait
currentHealthyWait time.Duration
healthyTimer *time.Timer healthyTimer *time.Timer
} }
const ( func NewServer(address string, settings configuration.Health,
defaultOpenvpnHealthyWaitTime = 6 * time.Second logger logging.Logger, openvpnLooper openvpn.Looper) Server {
openvpnHealthyWaitTimeAdd = 5 * time.Second
)
func NewServer(address string, logger logging.Logger,
openvpnLooper openvpn.Looper) Server {
return &server{ return &server{
address: address, address: address,
logger: logger, logger: logger,
@@ -43,7 +40,8 @@ func NewServer(address string, logger logging.Logger,
resolver: net.DefaultResolver, resolver: net.DefaultResolver,
openvpn: openvpnHealth{ openvpn: openvpnHealth{
looper: openvpnLooper, looper: openvpnLooper,
healthyWaitTime: defaultOpenvpnHealthyWaitTime, currentHealthyWait: settings.OpenVPN.Initial,
healthyWaitConfig: settings.OpenVPN,
}, },
} }
} }