Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion internal/healthcheck/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ func (c *Checker) Start(ctx context.Context) (runError <-chan error, err error)
func (c *Checker) Stop() error {
c.stop()
<-c.done
c.configMutex.Lock()
defer c.configMutex.Unlock()
Comment on lines +162 to +163

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because above we run c.stop() to cancel the long running goroutine and wait for <-c.done, the long running goroutine should be finished and so there should be no need to hold a mutex here I think. I'll dig into this, to avoid these AI-bandaid-fixes (that do the job, but it does bloat code whereas it does not fix the real source of the problem)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that after c.stop() and <-c.done the periodic goroutine can't touch these fields, so the lock in Stop isn't about that goroutine. What it guards is SetConfig: Stop runs on the collectHealthErrors goroutine (started at tunnelup.go:107, calls Stop at 148/156) while SetConfig runs on the VPN loop goroutine in onTunnelUp (tunnelup.go:88). If the loop state machine already guarantees those never overlap across a restart cycle, then the Stop hunk is redundant and I'm happy to drop it.

The part I'd keep either way is fullPeriodicCheck: on master it indexes c.tlsDialAddrs unlocked, while smallPeriodicCheck copies under the same mutex for the same fields, so this just makes the two checks consistent. The empty-slice guards also protect the try%len modulo from a divide-by-zero panic when the config is empty.

Happy to trim the PR to whichever subset you want after you've dug in.

c.tlsDialAddrs = nil
c.icmpTargetIPs = nil
c.smallCheckType = ""
Expand All @@ -170,6 +172,9 @@ func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
icmpTargetIPs := make([]netip.Addr, len(c.icmpTargetIPs))
copy(icmpTargetIPs, c.icmpTargetIPs)
c.configMutex.Unlock()
if c.smallCheckType != smallCheckDNS && len(icmpTargetIPs) == 0 {
return nil
}
tryTimeouts := []time.Duration{
5 * time.Second,
5 * time.Second,
Expand Down Expand Up @@ -202,11 +207,18 @@ func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
}

func (c *Checker) fullPeriodicCheck(ctx context.Context) error {
c.configMutex.Lock()
tlsDialAddrs := make([]string, len(c.tlsDialAddrs))
copy(tlsDialAddrs, c.tlsDialAddrs)
c.configMutex.Unlock()
if len(tlsDialAddrs) == 0 {
return nil
}
// 20s timeout in case the connection is under stress
// See https://github.com/qdm12/gluetun/issues/2270
tryTimeouts := []time.Duration{10 * time.Second, 15 * time.Second, 30 * time.Second}
check := func(ctx context.Context, try int) error {
tlsDialAddr := c.tlsDialAddrs[try%len(c.tlsDialAddrs)]
tlsDialAddr := tlsDialAddrs[try%len(tlsDialAddrs)]
return tcpTLSCheck(ctx, c.dialer, tlsDialAddr)
}
return withRetries(ctx, tryTimeouts, c.logger, "TCP+TLS dial", check)
Expand Down
14 changes: 14 additions & 0 deletions internal/healthcheck/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ func Test_Checker_fullcheck(t *testing.T) {
})
}

func Test_Checker_periodicCheckEmptyTargets(t *testing.T) {
t.Parallel()

checker := &Checker{smallCheckType: smallCheckICMP}

err := checker.fullPeriodicCheck(context.Background())

assert.NoError(t, err)

err = checker.smallPeriodicCheck(context.Background())

assert.NoError(t, err)
}

func Test_makeAddressToDial(t *testing.T) {
t.Parallel()

Expand Down