@@ -111,6 +111,34 @@ func TestOnTimeout_ContextCanceled(t *testing.T) {
111111 }
112112}
113113
114+ func TestIsRetryableTransient (t * testing.T ) {
115+ tests := []struct {
116+ name string
117+ err error
118+ want bool
119+ }{
120+ {"nil" , nil , false },
121+ {"timeout" , errors .New ("i/o timeout" ), true },
122+ {"connection refused" , errors .New ("connection refused" ), true },
123+ {"connect timeout" , errors .New ("connect timeout" ), true },
124+ {"connection reset" , errors .New ("connection reset by peer" ), true },
125+ {"status 503" , errors .New ("status 503: Service Unavailable" ), true },
126+ {"status 502" , errors .New ("status 502: Bad Gateway" ), true },
127+ {"internal server error" , errors .New ("Internal Server Error" ), true },
128+ {"http/500" , errors .New ("HTTP/500" ), true },
129+ {"bad gateway" , errors .New ("502 bad gateway" ), true },
130+ {"service temporarily unavailable" , errors .New ("503 Service Temporarily Unavailable" ), true },
131+ {"permanent error" , errors .New ("syntax error" ), false },
132+ }
133+ for _ , tt := range tests {
134+ t .Run (tt .name , func (t * testing.T ) {
135+ if got := IsRetryableTransient (tt .err ); got != tt .want {
136+ t .Errorf ("IsRetryableTransient() = %v, want %v" , got , tt .want )
137+ }
138+ })
139+ }
140+ }
141+
114142func TestIsTransientTrinoError (t * testing.T ) {
115143 tests := []struct {
116144 name string
@@ -238,3 +266,66 @@ func TestOnRetryableTrino_ConnectionRefusedThenSuccess(t *testing.T) {
238266 t .Errorf ("expected 2 calls (connection refused then success), got %d" , calls )
239267 }
240268}
269+
270+ func TestIsTransientClickHouseError (t * testing.T ) {
271+ tests := []struct {
272+ name string
273+ err error
274+ want bool
275+ }{
276+ {"nil" , nil , false },
277+ {"connection refused" , errors .New ("connect: connection refused" ), true },
278+ {"connect timeout" , errors .New ("connect timeout" ), true },
279+ {"TOO_MANY_PARTS" , errors .New ("DB::Exception: Too many parts" ), true },
280+ {"too many parts" , errors .New ("too many parts in total" ), true },
281+ {"memory_limit_exceeded" , errors .New ("Memory limit exceeded" ), true },
282+ {"memory limit" , errors .New ("Memory limit: would use 1.00 GiB" ), true },
283+ {"connection reset" , errors .New ("connection reset by peer" ), true },
284+ {"status 503" , errors .New ("status 503: Service Unavailable" ), true },
285+ {"status 502" , errors .New ("status 502: Bad Gateway" ), true },
286+ {"timeout" , errors .New ("i/o timeout" ), true },
287+ {"permanent error" , errors .New ("syntax error at position 5" ), false },
288+ }
289+ for _ , tt := range tests {
290+ t .Run (tt .name , func (t * testing.T ) {
291+ if got := IsTransientClickHouseError (tt .err ); got != tt .want {
292+ t .Errorf ("IsTransientClickHouseError() = %v, want %v" , got , tt .want )
293+ }
294+ })
295+ }
296+ }
297+
298+ func TestOnRetryableClickHouse_TransientThenSuccess (t * testing.T ) {
299+ ctx := context .Background ()
300+ calls := 0
301+ transientErr := errors .New ("DB::Exception: Too many parts" )
302+ err := OnRetryableClickHouse (ctx , 3 , 5 * time .Millisecond , func () error {
303+ calls ++
304+ if calls < 2 {
305+ return transientErr
306+ }
307+ return nil
308+ })
309+ if err != nil {
310+ t .Errorf ("OnRetryableClickHouse() err = %v, want nil" , err )
311+ }
312+ if calls != 2 {
313+ t .Errorf ("expected 2 calls (retry then success), got %d" , calls )
314+ }
315+ }
316+
317+ func TestOnRetryableClickHouse_PermanentErrorNoRetry (t * testing.T ) {
318+ ctx := context .Background ()
319+ wantErr := errors .New ("syntax error at position 5" )
320+ calls := 0
321+ err := OnRetryableClickHouse (ctx , 3 , 10 * time .Millisecond , func () error {
322+ calls ++
323+ return wantErr
324+ })
325+ if ! errors .Is (err , wantErr ) {
326+ t .Errorf ("OnRetryableClickHouse() err = %v, want %v" , err , wantErr )
327+ }
328+ if calls != 1 {
329+ t .Errorf ("expected 1 call (no retry on permanent error), got %d" , calls )
330+ }
331+ }
0 commit comments