Skip to content

Commit 6069d82

Browse files
authored
Handle status code infinite loop (#162)
1 parent 4e30f16 commit 6069d82

5 files changed

Lines changed: 248 additions & 12 deletions

File tree

pkg/common/helper.go

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ package common
2323
import (
2424
"context"
2525
"errors"
26+
"net/http"
2627
"reflect"
2728
"runtime"
2829
"strings"
2930
"time"
3031

3132
nuclioerrors "github.com/nuclio/errors"
3233
"github.com/nuclio/logger"
34+
v3ioerrors "github.com/v3io/v3io-go/pkg/errors"
3335
)
3436

3537
func getFunctionName(fn interface{}) string {
@@ -184,24 +186,56 @@ func StringSlicesEqual(slice1 []string, slice2 []string) bool {
184186
return true
185187
}
186188

189+
// EngineErrorIsNonFatal checks whether the error should be considered non-fatal
190+
// It unwraps the error chain and checks each error against predefined non-fatal patterns
187191
func EngineErrorIsNonFatal(err error) bool {
188-
var nonFatalEngineErrorsPartialMatch = []string{
189-
"dialing to the given TCP address timed out",
190-
"timeout",
191-
"refused",
192+
nonFatalErrorCheckFunctions := []func(error) bool{
193+
isNonFatalErrorString,
194+
isNonFatalStatusCode,
192195
}
193-
return errorMatches(err, nonFatalEngineErrorsPartialMatch)
196+
return errorMatches(err, nonFatalErrorCheckFunctions)
194197
}
195198

196-
func errorMatches(err error, substrings []string) bool {
199+
// errorMatches unwraps the error chain and applies each check function to every error in the chain
200+
// Returns true if any check function returns true for any error in the chain
201+
func errorMatches(err error, checkFunctions []func(error) bool) bool {
197202
// Unwraps the entire error chain
198203
for e := err; e != nil; e = errors.Unwrap(e) {
199-
errMsg := e.Error()
200-
for _, substring := range substrings {
201-
if strings.Contains(errMsg, substring) {
204+
// Execute each check function on the current error
205+
for _, checkFunc := range checkFunctions {
206+
if checkFunc(e) {
202207
return true
203208
}
204209
}
205210
}
206211
return false
207212
}
213+
214+
// isNonFatalErrorString checks whether the error message contains any of the predefined non-fatal substrings
215+
func isNonFatalErrorString(err error) bool {
216+
var nonFatalEngineErrorsPartialMatch = []string{
217+
"dialing to the given TCP address timed out",
218+
"timeout",
219+
"refused",
220+
}
221+
errMsg := err.Error()
222+
for _, substring := range nonFatalEngineErrorsPartialMatch {
223+
if strings.Contains(errMsg, substring) {
224+
return true
225+
}
226+
}
227+
return false
228+
}
229+
230+
// isNonFatalStatusCode checks whether the error contains any of the predefined non-fatal HTTP status codes
231+
func isNonFatalStatusCode(err error) bool {
232+
var nonFatalStatusCodes = []int{
233+
http.StatusServiceUnavailable,
234+
}
235+
errWithStatusCode, ok := err.(v3ioerrors.ErrorWithStatusCode)
236+
if !ok {
237+
return false
238+
}
239+
statusCode := errWithStatusCode.StatusCode()
240+
return IntSliceContainsInt(nonFatalStatusCodes, statusCode)
241+
}

pkg/common/helper_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
Copyright 2025 Iguazio Systems Ltd.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License") with
5+
an addition restriction as set forth herein. You may not use this
6+
file except in compliance with the License. You may obtain a copy of
7+
the License at http://www.apache.org/licenses/LICENSE-2.0.
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
implied. See the License for the specific language governing
13+
permissions and limitations under the License.
14+
15+
In addition, you may not use the software for any purposes that are
16+
illegal under applicable law, and the grant of the foregoing license
17+
under the Apache 2.0 license is conditioned upon your compliance with
18+
such restriction.
19+
*/
20+
21+
package common
22+
23+
import (
24+
"fmt"
25+
"testing"
26+
27+
"github.com/nuclio/errors"
28+
v3ioerrors "github.com/v3io/v3io-go/pkg/errors"
29+
30+
"github.com/stretchr/testify/suite"
31+
)
32+
33+
type helperTestSuite struct {
34+
suite.Suite
35+
}
36+
37+
func (suite *helperTestSuite) TestEngineErrorIsNonFatalNestedErrorFromLog() {
38+
// Create the nested error structure with http 503 Service Temporarily Unavailable as the root cause
39+
40+
// Create HTTP response string contains the 503 error
41+
httpResponseStr := `HTTP/1.1 503 Service Temporarily Unavailable\r\nServer: nginx\r\nDate: Tue, 03 Jun 2025 07:37:09 GMT\r\nContent-Type: application/json\r\nContent-Length: 89\r\nConnection: keep-alive\r\n\r\n{\n\t\"ErrorCode\": -117440512,\n\t\"ErrorMessage\": \"Failed to send a control message request\"\n}`
42+
sanitizedRequest := `PUT /projects/perform044wp2gqixmhkv1/datafetch_output_stream/20 HTTP/1.1\r\nUser-Agent: fasthttp\r\nHost: v3io-webapi:8081\r\nContent-Type: application/json\r\nContent-Length: 58\r\nX-V3io-Session-Key: SANITIZED\r\nX-V3io-Function: GetItem\r\n\r\n{\"AttributesToGet\": \"__serving_committed_sequence_number\"}`
43+
httpBody := fmt.Errorf("Expected a 2xx response status code: %s\nRequest details:\n%s",
44+
httpResponseStr, sanitizedRequest)
45+
46+
// Wrap with ErrorWithStatusCode
47+
statusCodeErr := v3ioerrors.NewErrorWithStatusCode(httpBody, 503)
48+
49+
// Further wrap the error to simulate the nested error chain
50+
shardItemErr := errors.Wrap(statusCodeErr, "Failed getting shard item")
51+
sequenceNumberErr := errors.Wrap(shardItemErr, "Failed to get shard sequenceNumber from item attributes")
52+
persistencyErr := errors.Wrap(sequenceNumberErr, "Failed to get shard location from persistency")
53+
locationErr := errors.Wrap(persistencyErr, "Failed to get shard location")
54+
finalErr := errors.Wrapf(locationErr, "Failed to get shard location state, attempts exhausted. shard id: %d", 20)
55+
56+
// Test that EngineErrorIsNonFatal correctly unwraps the nested error chain
57+
// Since status code 503 is now in nonFatalStatusCodes, expect true
58+
result := EngineErrorIsNonFatal(finalErr)
59+
suite.Require().True(result, "Expected EngineErrorIsNonFatal to return true (503 is in nonFatalStatusCodes)")
60+
}
61+
62+
func (suite *helperTestSuite) TestMatchErrorString() {
63+
for _, testCase := range []struct {
64+
name string
65+
errMsg string
66+
expected bool
67+
}{
68+
{
69+
name: "timeout error",
70+
errMsg: "connection timeout occurred",
71+
expected: true,
72+
},
73+
{
74+
name: "dial timeout error",
75+
errMsg: "dialing to the given TCP address timed out",
76+
expected: true,
77+
},
78+
{
79+
name: "connection refused",
80+
errMsg: "connection refused",
81+
expected: true,
82+
},
83+
{
84+
name: "generic error",
85+
errMsg: "something went wrong",
86+
expected: false,
87+
},
88+
{
89+
name: "empty error",
90+
errMsg: "",
91+
expected: false,
92+
},
93+
} {
94+
suite.Run(testCase.name, func() {
95+
err := fmt.Errorf(testCase.errMsg)
96+
result := isNonFatalErrorString(err)
97+
suite.Require().Equal(testCase.expected, result)
98+
})
99+
}
100+
}
101+
102+
func (suite *helperTestSuite) TestMatchErrorStatusCode() {
103+
baseErr := fmt.Errorf("test error")
104+
for _, testCase := range []struct {
105+
name string
106+
statusCode int
107+
expected bool
108+
}{
109+
{
110+
name: "non-fatal status code 503",
111+
statusCode: 503,
112+
expected: true,
113+
},
114+
{
115+
name: "fatal status code 500",
116+
statusCode: 500,
117+
expected: false,
118+
},
119+
{
120+
name: "fatal status code 404",
121+
statusCode: 404,
122+
expected: false,
123+
},
124+
{
125+
name: "fatal status code 200",
126+
statusCode: 200,
127+
expected: false,
128+
},
129+
} {
130+
suite.Run(testCase.name, func() {
131+
err := v3ioerrors.NewErrorWithStatusCode(baseErr, testCase.statusCode)
132+
result := isNonFatalStatusCode(err)
133+
suite.Require().Equal(testCase.expected, result)
134+
})
135+
}
136+
}
137+
138+
func (suite *helperTestSuite) TestMatchErrorStatusCodeNonV3ioError() {
139+
err := fmt.Errorf("regular error")
140+
result := isNonFatalStatusCode(err)
141+
suite.Require().False(result)
142+
}
143+
144+
func (suite *helperTestSuite) TestEngineErrorIsNonFatalStringMatch() {
145+
for _, testCase := range []struct {
146+
name string
147+
errMsg string
148+
expected bool
149+
}{
150+
{
151+
name: "timeout error",
152+
errMsg: "operation timeout",
153+
expected: true,
154+
},
155+
{
156+
name: "connection refused",
157+
errMsg: "connection refused by server",
158+
expected: true,
159+
},
160+
{
161+
name: "generic error",
162+
errMsg: "something went wrong",
163+
expected: false,
164+
},
165+
} {
166+
suite.Run(testCase.name, func() {
167+
err := fmt.Errorf(testCase.errMsg)
168+
result := EngineErrorIsNonFatal(err)
169+
suite.Require().Equal(testCase.expected, result)
170+
})
171+
}
172+
}
173+
174+
func (suite *helperTestSuite) TestEngineErrorIsNonFatalStatusCodeMatch() {
175+
baseErr := fmt.Errorf("test error")
176+
for _, testCase := range []struct {
177+
name string
178+
statusCode int
179+
expected bool
180+
}{
181+
{
182+
name: "Service Temporarily Unavailable error",
183+
statusCode: 503,
184+
expected: true,
185+
}, {
186+
name: "no error",
187+
statusCode: 200,
188+
expected: false,
189+
},
190+
} {
191+
suite.Run(testCase.name, func() {
192+
err := v3ioerrors.NewErrorWithStatusCode(baseErr, testCase.statusCode)
193+
result := EngineErrorIsNonFatal(err)
194+
suite.Require().Equal(testCase.expected, result)
195+
})
196+
}
197+
198+
}
199+
200+
func TestHelperTestSuite(t *testing.T) {
201+
suite.Run(t, new(helperTestSuite))
202+
}

pkg/dataplane/http/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ func (c *context) GetItemsSync(getItemsInput *v3io.GetItemsInput) (*v3io.Respons
360360
}
361361

362362
if len(getItemsInput.DataPlaneInput.MtimeSec) > 0 { //nolint:staticcheck // QF1008
363-
headers["conditional-mtime-sec"] = getItemsInput.DataPlaneInput.MtimeSec //nolint:staticcheck // QF1008
363+
headers["conditional-mtime-sec"] = getItemsInput.DataPlaneInput.MtimeSec //nolint:staticcheck // QF1008
364364
headers["conditional-mtime-nsec"] = getItemsInput.DataPlaneInput.MtimeNsec //nolint:staticcheck // QF1008
365365
}
366366

pkg/dataplane/itemscursor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type ItemsCursor struct {
4343
scattered bool
4444

4545
logger logger.Logger //nolint:unused
46-
retryAttempts int //nolint:unused
46+
retryAttempts int //nolint:unused
4747
retryInterval time.Duration //nolint:unused
4848
}
4949

pkg/errors/errors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
)
2626

2727
var ErrInvalidTypeConversion = errors.New("Invalid type conversion") //nolint:staticcheck // ST1005
28-
var ErrNotFound = errors.New("Not found") //nolint:staticcheck // ST1005
28+
var ErrNotFound = errors.New("Not found") //nolint:staticcheck // ST1005
2929
var ErrStopped = errors.New("Stopped")
3030
var ErrTimeout = errors.New("Timed out") //nolint:staticcheck // ST1005
3131

0 commit comments

Comments
 (0)