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
71 changes: 71 additions & 0 deletions tunnel/retry_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package tunnel

import (
"context"
"errors"
"net"
"os"
"testing"
)

type namedRetryError struct {
name string
err error
}

func TestShouldStopRetryLocalResourceExhaustion(t *testing.T) {
for _, test := range localResourceExhaustionTestCases() {
t.Run(test.name+"/direct", func(t *testing.T) {
if !shouldStopRetry(test.err) {
t.Fatal("shouldStopRetry() = false, want true")
}
})

t.Run(test.name+"/wrapped", func(t *testing.T) {
err := &net.OpError{
Op: "dial",
Net: "tcp",
Err: &os.SyscallError{Syscall: "connect", Err: test.err},
}
if !shouldStopRetry(err) {
t.Fatal("shouldStopRetry() = false, want true")
}
})
}
}

func TestRetryStopsOnLocalResourceExhaustion(t *testing.T) {
resourceErr := localResourceExhaustionTestCases()[0].err
attempts := 0
_, err := retry(context.Background(), func(context.Context) (struct{}, error) {
attempts++
return struct{}{}, resourceErr
}, nil)
if !errors.Is(err, resourceErr) {
t.Fatalf("retry() error = %v, want %v", err, resourceErr)
}
if attempts != 1 {
t.Fatalf("retry() attempts = %d, want 1", attempts)
}
}

func TestRetryPreservesTransientErrorBehavior(t *testing.T) {
transientErr := errors.New("transient network error")
attempts := 0
result, err := retry(context.Background(), func(context.Context) (string, error) {
attempts++
if attempts == 1 {
return "", transientErr
}
return "connected", nil
}, nil)
if err != nil {
t.Fatalf("retry() error = %v, want nil", err)
}
if result != "connected" {
t.Fatalf("retry() result = %q, want connected", result)
}
if attempts != 2 {
t.Fatalf("retry() attempts = %d, want 2", attempts)
}
}
15 changes: 15 additions & 0 deletions tunnel/retry_error_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build !windows

package tunnel

import (
"errors"
"syscall"
)

func isLocalResourceExhaustion(err error) bool {
return errors.Is(err, syscall.EADDRNOTAVAIL) ||
errors.Is(err, syscall.EMFILE) ||
errors.Is(err, syscall.ENFILE) ||
errors.Is(err, syscall.ENOBUFS)
}
14 changes: 14 additions & 0 deletions tunnel/retry_error_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !windows

package tunnel

import "syscall"

func localResourceExhaustionTestCases() []namedRetryError {
return []namedRetryError{
{name: "EADDRNOTAVAIL", err: syscall.EADDRNOTAVAIL},
{name: "EMFILE", err: syscall.EMFILE},
{name: "ENFILE", err: syscall.ENFILE},
{name: "ENOBUFS", err: syscall.ENOBUFS},
}
}
16 changes: 16 additions & 0 deletions tunnel/retry_error_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build windows

package tunnel

import (
"errors"

"golang.org/x/sys/windows"
)

func isLocalResourceExhaustion(err error) bool {
return errors.Is(err, windows.WSAEADDRNOTAVAIL) ||
errors.Is(err, windows.WSAEMFILE) ||
errors.Is(err, windows.ERROR_TOO_MANY_OPEN_FILES) ||
errors.Is(err, windows.WSAENOBUFS)
}
14 changes: 14 additions & 0 deletions tunnel/retry_error_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build windows

package tunnel

import "golang.org/x/sys/windows"

func localResourceExhaustionTestCases() []namedRetryError {
return []namedRetryError{
{name: "WSAEADDRNOTAVAIL", err: windows.WSAEADDRNOTAVAIL},
{name: "WSAEMFILE", err: windows.WSAEMFILE},
{name: "ERROR_TOO_MANY_OPEN_FILES", err: windows.ERROR_TOO_MANY_OPEN_FILES},
{name: "WSAENOBUFS", err: windows.WSAENOBUFS},
}
}
3 changes: 3 additions & 0 deletions tunnel/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,9 @@ func getRules(metadata *C.Metadata) []C.Rule {
}

func shouldStopRetry(err error) bool {
if isLocalResourceExhaustion(err) {
return true
}
if errors.Is(err, resolver.ErrIPNotFound) {
return true
}
Expand Down