diff --git a/tunnel/retry_error_test.go b/tunnel/retry_error_test.go new file mode 100644 index 0000000000..0d0c8b71f9 --- /dev/null +++ b/tunnel/retry_error_test.go @@ -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) + } +} diff --git a/tunnel/retry_error_unix.go b/tunnel/retry_error_unix.go new file mode 100644 index 0000000000..48b3440ec1 --- /dev/null +++ b/tunnel/retry_error_unix.go @@ -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) +} diff --git a/tunnel/retry_error_unix_test.go b/tunnel/retry_error_unix_test.go new file mode 100644 index 0000000000..3152e3d65c --- /dev/null +++ b/tunnel/retry_error_unix_test.go @@ -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}, + } +} diff --git a/tunnel/retry_error_windows.go b/tunnel/retry_error_windows.go new file mode 100644 index 0000000000..3be661fc2a --- /dev/null +++ b/tunnel/retry_error_windows.go @@ -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) +} diff --git a/tunnel/retry_error_windows_test.go b/tunnel/retry_error_windows_test.go new file mode 100644 index 0000000000..29d6865ebb --- /dev/null +++ b/tunnel/retry_error_windows_test.go @@ -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}, + } +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 86dbc14ae4..7b0c0ff896 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -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 }