Skip to content

Commit b1c96fb

Browse files
committed
fix: remove back pressure and add retryable error
1 parent 2dda0b2 commit b1c96fb

File tree

3 files changed

+74
-45
lines changed

3 files changed

+74
-45
lines changed

exporter/exporterhelper/internal/experr/back_pressure.go renamed to exporter/exporterhelper/internal/experr/retryable_error.go

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"errors"
99
"net"
1010
"net/http"
11-
"strings"
11+
"regexp"
1212

1313
"google.golang.org/grpc/codes"
1414
"google.golang.org/grpc/status"
@@ -29,18 +29,30 @@ const (
2929
ReasonHTTP504 Reason = "http_504"
3030
ReasonHTTPRetryAfter Reason = "http_retry_after"
3131
ReasonNetTimeout Reason = "net_timeout"
32-
ReasonTextBackpressure Reason = "text_backpressure"
32+
ReasonTextCircuitBreaker Reason = "text:circuit_breaker"
33+
ReasonTextOverload Reason = "text:overload"
34+
ReasonTextBackpressure Reason = "text:backpressure"
3335
)
3436

35-
// ClassifyBackpressure returns (isBackpressure, reason).
37+
var fallbackRegex = regexp.MustCompile(
38+
`(?i)(429|too many requests)|` + // ReasonHTTP429
39+
`(503|service unavailable)|` + // ReasonHTTP503
40+
`(504|gateway timeout|upstream timeout)|` + // ReasonHTTP504
41+
`(retry-after)|` + // ReasonHTTPRetryAfter
42+
`(circuit breaker)|` + // ReasonTextCircuitBreaker
43+
`(overload)|` + // ReasonTextOverload
44+
`(backpressure)`, // ReasonTextBackpressure
45+
)
46+
47+
// ClassifyErrorReason returns (isRetryable, reason).
3648
// The logic is strictly:
3749
// 1. Typed signals first (fast, no allocs)
38-
// 2. Fallback to a single lowercase + a few contains checks
50+
// 2. Fallback to a single regex match on the error string
3951
//
4052
// NOTE: This function runs only on error paths. Even then,
4153
// typed checks return early. Fallback string scanning is a
4254
// micro-cost compared to network I/O and retry backoff.
43-
func ClassifyBackpressure(err error) (bool, Reason) {
55+
func ClassifyErrorReason(err error) (bool, Reason) {
4456
if err == nil {
4557
return false, ""
4658
}
@@ -88,30 +100,37 @@ func ClassifyBackpressure(err error) (bool, Reason) {
88100
}
89101
}
90102

91-
// 5) Fallback: cheap lowercase + tight substring checks.
92-
// Keep this list intentionally small to minimize false positives.
93-
s := strings.ToLower(err.Error())
94-
if strings.Contains(s, "429") || strings.Contains(s, "too many requests") {
95-
return true, ReasonHTTP429
103+
// 5) Fallback: cheap regex match on error string.
104+
matches := fallbackRegex.FindStringSubmatch(err.Error())
105+
if matches == nil {
106+
return false, ""
96107
}
97-
if strings.Contains(s, "503") || strings.Contains(s, "service unavailable") {
108+
109+
// The regex has 7 capture groups, one for each reason.
110+
// FindStringSubmatch returns the full match at [0], then groups at [1]...[n].
111+
// We check which group captured.
112+
switch {
113+
case matches[1] != "":
114+
return true, ReasonHTTP429
115+
case matches[2] != "":
98116
return true, ReasonHTTP503
99-
}
100-
if strings.Contains(s, "504") || strings.Contains(s, "gateway timeout") || strings.Contains(s, "upstream timeout") {
117+
case matches[3] != "":
101118
return true, ReasonHTTP504
102-
}
103-
if strings.Contains(s, "retry-after") {
119+
case matches[4] != "":
104120
return true, ReasonHTTPRetryAfter
105-
}
106-
if strings.Contains(s, "circuit breaker") || strings.Contains(s, "overload") || strings.Contains(s, "backpressure") {
121+
case matches[5] != "":
122+
return true, ReasonTextCircuitBreaker
123+
case matches[6] != "":
124+
return true, ReasonTextOverload
125+
case matches[7] != "":
107126
return true, ReasonTextBackpressure
108127
}
109128

110129
return false, ""
111130
}
112131

113-
// IsBackpressure is a convenience wrapper.
114-
func IsBackpressure(err error) bool {
115-
ok, _ := ClassifyBackpressure(err)
132+
// IsRetryableError is a convenience wrapper.
133+
func IsRetryableError(err error) bool {
134+
ok, _ := ClassifyErrorReason(err)
116135
return ok
117136
}

exporter/exporterhelper/internal/experr/back_pressure_test.go renamed to exporter/exporterhelper/internal/experr/retryable_error_test.go

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,19 @@ func lower(msg string) error { return errors.New(strings.ToLower(msg)) }
3939

4040
// ----- tests -----
4141

42-
func TestClassifyBackpressure_TypedContextDeadline(t *testing.T) {
43-
ok, reason := ClassifyBackpressure(context.DeadlineExceeded)
42+
func TestClassifyErrorReason_TypedContextDeadline(t *testing.T) {
43+
ok, reason := ClassifyErrorReason(context.DeadlineExceeded)
4444
assert.True(t, ok)
4545
assert.Equal(t, ReasonDeadline, reason)
4646
}
4747

48-
func TestClassifyBackpressure_TypedNetTimeout(t *testing.T) {
49-
ok, reason := ClassifyBackpressure(netTimeoutErr{})
48+
func TestClassifyErrorReason_TypedNetTimeout(t *testing.T) {
49+
ok, reason := ClassifyErrorReason(netTimeoutErr{})
5050
assert.True(t, ok)
5151
assert.Equal(t, ReasonNetTimeout, reason)
5252
}
5353

54-
func TestClassifyBackpressure_TypedGRPC_StatusCodes(t *testing.T) {
54+
func TestClassifyErrorReason_TypedGRPC_StatusCodes(t *testing.T) {
5555
cases := []struct {
5656
err error
5757
reason Reason
@@ -62,13 +62,13 @@ func TestClassifyBackpressure_TypedGRPC_StatusCodes(t *testing.T) {
6262
}
6363

6464
for _, tc := range cases {
65-
ok, reason := ClassifyBackpressure(tc.err)
65+
ok, reason := ClassifyErrorReason(tc.err)
6666
assert.True(t, ok, tc.err)
6767
assert.Equal(t, tc.reason, reason)
6868
}
6969
}
7070

71-
func TestClassifyBackpressure_TypedHTTP_StatusCodes(t *testing.T) {
71+
func TestClassifyErrorReason_TypedHTTP_StatusCodes(t *testing.T) {
7272
cases := []struct {
7373
code int
7474
header http.Header
@@ -83,64 +83,74 @@ func TestClassifyBackpressure_TypedHTTP_StatusCodes(t *testing.T) {
8383
for _, tc := range cases {
8484
resp := &http.Response{StatusCode: tc.code, Header: tc.header}
8585
err := httpRespErr{resp: resp, msg: "transport error"}
86-
ok, reason := ClassifyBackpressure(err)
86+
ok, reason := ClassifyErrorReason(err)
8787
assert.True(t, ok, tc)
8888
assert.Equal(t, tc.reason, reason, tc)
8989
}
9090
}
9191

92-
func TestClassifyBackpressure_FallbackStrings_Positive(t *testing.T) {
93-
// These exercise the lowercase + contains path.
92+
func TestClassifyErrorReason_FallbackRegex_Positive(t *testing.T) {
93+
// These exercise the regex fallback path.
9494
cases := []struct {
9595
err error
9696
reason Reason
9797
}{
9898
{errors.New("HTTP 429 Too Many Requests"), ReasonHTTP429},
9999
{lower("too many requests"), ReasonHTTP429},
100+
{errors.New("some 429 error"), ReasonHTTP429},
100101

101102
{errors.New("received HTTP 503 Service Unavailable"), ReasonHTTP503},
102103
{lower("service unavailable"), ReasonHTTP503},
104+
{errors.New("503 blah"), ReasonHTTP503},
103105

104106
{errors.New("HTTP 504 Gateway Timeout"), ReasonHTTP504},
105107
{lower("gateway timeout"), ReasonHTTP504},
106108
{lower("upstream timeout"), ReasonHTTP504},
109+
{errors.New("got a 504"), ReasonHTTP504},
107110

108111
{lower("retry-after header present"), ReasonHTTPRetryAfter},
112+
{errors.New("Retry-After: 30"), ReasonHTTPRetryAfter},
113+
114+
{lower("circuit breaker open"), ReasonTextCircuitBreaker},
115+
{errors.New("Circuit Breaker"), ReasonTextCircuitBreaker},
116+
117+
{lower("system overload"), ReasonTextOverload},
118+
{errors.New("OVERLOAD"), ReasonTextOverload},
109119

110-
{lower("circuit breaker open"), ReasonTextBackpressure},
111-
{lower("system overload"), ReasonTextBackpressure},
112120
{lower("backpressure detected"), ReasonTextBackpressure},
121+
{errors.New("BACKPRESSURE"), ReasonTextBackpressure},
113122
}
114123

115124
for _, tc := range cases {
116-
ok, reason := ClassifyBackpressure(tc.err)
125+
ok, reason := ClassifyErrorReason(tc.err)
117126
assert.True(t, ok, tc.err)
118127
assert.Equal(t, tc.reason, reason, tc.err)
119128
}
120129
}
121130

122-
func TestIsBackpressure_Positive_Smoke(t *testing.T) {
131+
func TestIsRetryableError_Positive_Smoke(t *testing.T) {
123132
// A quick smoke test through the convenience wrapper.
124-
assert.True(t, IsBackpressure(status.Error(codes.ResourceExhausted, "x")))
125-
assert.True(t, IsBackpressure(netTimeoutErr{}))
126-
assert.True(t, IsBackpressure(context.DeadlineExceeded))
127-
assert.True(t, IsBackpressure(errors.New("429")))
128-
assert.True(t, IsBackpressure(errors.New("service unavailable")))
129-
assert.True(t, IsBackpressure(errors.New("gateway timeout")))
133+
assert.True(t, IsRetryableError(status.Error(codes.ResourceExhausted, "x")))
134+
assert.True(t, IsRetryableError(netTimeoutErr{}))
135+
assert.True(t, IsRetryableError(context.DeadlineExceeded))
136+
assert.True(t, IsRetryableError(errors.New("429")))
137+
assert.True(t, IsRetryableError(errors.New("service unavailable")))
138+
assert.True(t, IsRetryableError(errors.New("gateway timeout")))
139+
assert.True(t, IsRetryableError(errors.New("OVERLOAD")))
130140
}
131141

132-
func TestIsBackpressure_NegativeCases(t *testing.T) {
142+
func TestIsRetryableError_NegativeCases(t *testing.T) {
133143
cases := []error{
134144
nil,
135145
errors.New("some transient error"),
136-
errors.New("internal server error"), // intentionally not treated as backpressure
137-
errors.New("permission denied"), // not backpressure
146+
errors.New("internal server error"), // intentionally not treated as retryable
147+
errors.New("permission denied"), // not retryable
138148
errors.New("bad gateway (502)"), // not in our fallback allowlist
139149
errors.New("request timeout (408)"), // not in our fallback allowlist
140150
errors.New("proxy timeout (599)"), // not in our fallback allowlist
141151
}
142152

143153
for _, err := range cases {
144-
assert.False(t, IsBackpressure(err), "expected not backpressure: %v", err)
154+
assert.False(t, IsRetryableError(err), "expected not retryable: %v", err)
145155
}
146156
}

exporter/exporterhelper/internal/queue_sender.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func NewQueueSender(
106106
err := origExportFunc(ctx, req)
107107
rtt := time.Since(startTime)
108108

109-
isBackpressure := experr.IsBackpressure(err)
109+
isBackpressure := experr.IsRetryableError(err)
110110

111111
isSuccess := err == nil
112112

0 commit comments

Comments
 (0)