Skip to content

Commit 5f3fc19

Browse files
[Gemini] Add retry filter support to the Java Remote RetryHandler (#39004)
* Add retry filter support to the Java Remote RetryHandler * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * spotless --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 2f7d95d commit 5f3fc19

2 files changed

Lines changed: 170 additions & 2 deletions

File tree

sdks/java/ml/inference/remote/src/main/java/org/apache/beam/sdk/ml/inference/remote/RetryHandler.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,42 @@ public class RetryHandler implements Serializable {
3434
private final Duration initialBackoff;
3535
private final Duration maxBackoff;
3636
private final Duration maxCumulativeBackoff;
37+
private final RetryFilter retryFilter;
38+
39+
@FunctionalInterface
40+
public interface RetryFilter extends Serializable {
41+
boolean shouldRetry(Exception e);
42+
}
3743

3844
private RetryHandler(
39-
int maxRetries, Duration initialBackoff, Duration maxBackoff, Duration maxCumulativeBackoff) {
45+
int maxRetries,
46+
Duration initialBackoff,
47+
Duration maxBackoff,
48+
Duration maxCumulativeBackoff,
49+
RetryFilter retryFilter) {
4050
this.maxRetries = maxRetries;
4151
this.initialBackoff = initialBackoff;
4252
this.maxBackoff = maxBackoff;
4353
this.maxCumulativeBackoff = maxCumulativeBackoff;
54+
this.retryFilter =
55+
java.util.Objects.requireNonNull(retryFilter, "retryFilter must not be null");
4456
}
4557

4658
public static RetryHandler withDefaults() {
4759
return new RetryHandler(
4860
3, // maxRetries
4961
Duration.standardSeconds(1), // initialBackoff
5062
Duration.standardSeconds(10), // maxBackoff per retry
51-
Duration.standardMinutes(1) // maxCumulativeBackoff
63+
Duration.standardMinutes(1), // maxCumulativeBackoff
64+
e -> true // retryFilter default to retrying all exceptions
5265
);
5366
}
5467

68+
public RetryHandler withRetryFilter(RetryFilter retryFilter) {
69+
return new RetryHandler(
70+
maxRetries, initialBackoff, maxBackoff, maxCumulativeBackoff, retryFilter);
71+
}
72+
5573
public <T> T execute(RetryableRequest<T> request) throws Exception {
5674
BackOff backoff =
5775
FluentBackoff.DEFAULT
@@ -72,6 +90,11 @@ public <T> T execute(RetryableRequest<T> request) throws Exception {
7290
} catch (Exception e) {
7391
lastException = e;
7492

93+
if (retryFilter != null && !retryFilter.shouldRetry(e)) {
94+
LOG.warn("Exception not eligible for retry. Failing immediately.", e);
95+
throw e;
96+
}
97+
7598
long backoffMillis = backoff.nextBackOffMillis();
7699

77100
if (backoffMillis == BackOff.STOP) {
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.ml.inference.remote;
19+
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertThrows;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import java.util.concurrent.atomic.AtomicInteger;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
import org.junit.runners.JUnit4;
28+
29+
@RunWith(JUnit4.class)
30+
public class RetryHandlerTest {
31+
32+
private static class NonRetryableException extends Exception {
33+
NonRetryableException(String message) {
34+
super(message);
35+
}
36+
}
37+
38+
private static class RetryableException extends Exception {
39+
RetryableException(String message) {
40+
super(message);
41+
}
42+
}
43+
44+
@Test
45+
public void testRetryWithDefaultFilter() throws Exception {
46+
RetryHandler handler = RetryHandler.withDefaults();
47+
AtomicInteger attempts = new AtomicInteger(0);
48+
49+
RuntimeException thrown =
50+
assertThrows(
51+
RuntimeException.class,
52+
() ->
53+
handler.execute(
54+
() -> {
55+
attempts.incrementAndGet();
56+
throw new Exception("Always fails");
57+
}));
58+
59+
assertTrue(thrown.getMessage().contains("exhausting retries"));
60+
assertEquals(4, attempts.get()); // 1 initial attempt + 3 retries
61+
}
62+
63+
@Test
64+
public void testRetryWithCustomFilter_ShouldNotRetry() {
65+
RetryHandler handler =
66+
RetryHandler.withDefaults()
67+
.withRetryFilter(
68+
e -> {
69+
if (e instanceof NonRetryableException) {
70+
return false;
71+
}
72+
return true;
73+
});
74+
75+
AtomicInteger attempts = new AtomicInteger(0);
76+
77+
NonRetryableException thrown =
78+
assertThrows(
79+
NonRetryableException.class,
80+
() ->
81+
handler.execute(
82+
() -> {
83+
attempts.incrementAndGet();
84+
throw new NonRetryableException("Should not retry");
85+
}));
86+
87+
assertEquals("Should not retry", thrown.getMessage());
88+
assertEquals(1, attempts.get()); // 1 initial attempt, no retries
89+
}
90+
91+
@Test
92+
public void testRetryWithCustomFilter_ShouldRetry() {
93+
RetryHandler handler =
94+
RetryHandler.withDefaults()
95+
.withRetryFilter(
96+
e -> {
97+
if (e instanceof NonRetryableException) {
98+
return false;
99+
}
100+
return true;
101+
});
102+
103+
AtomicInteger attempts = new AtomicInteger(0);
104+
105+
RuntimeException thrown =
106+
assertThrows(
107+
RuntimeException.class,
108+
() ->
109+
handler.execute(
110+
() -> {
111+
attempts.incrementAndGet();
112+
throw new RetryableException("Should retry");
113+
}));
114+
115+
assertTrue(thrown.getMessage().contains("exhausting retries"));
116+
assertEquals(4, attempts.get()); // 1 initial attempt + 3 retries
117+
}
118+
119+
@Test
120+
public void testRetryWithCustomFilter_EventualSuccess() throws Exception {
121+
RetryHandler handler =
122+
RetryHandler.withDefaults()
123+
.withRetryFilter(
124+
e -> {
125+
if (e instanceof NonRetryableException) {
126+
return false;
127+
}
128+
return true;
129+
});
130+
131+
AtomicInteger attempts = new AtomicInteger(0);
132+
133+
String result =
134+
handler.execute(
135+
() -> {
136+
if (attempts.incrementAndGet() < 3) {
137+
throw new RetryableException("Temporary failure");
138+
}
139+
return "success";
140+
});
141+
142+
assertEquals("success", result);
143+
assertEquals(3, attempts.get()); // 1 initial attempt + 2 retries
144+
}
145+
}

0 commit comments

Comments
 (0)