Skip to content

Commit fde7d31

Browse files
snicollartembilan
authored andcommitted
GH-4051: Replace Spring Retry usage to core retry
Fixes: #4051 This commit replaces Spring Retry by the core retry support introduced in Spring Framework 7. This is a breaking change mostly in configuration that is detailed below. The main feature in Spring Kafka is BackOffValuesGenerator that generates the required BackOff values upfront. These are then managed by the listener infrastructure and Spring Retry is no longer involved. Moving this code from BackOffPolicy to BackOff dramatically simplifies that class as Spring Framework's core API naturally provides this information without the need of an extra infrastructure. From a configuration standpoint, Spring Kafka relies quite heavily on Spring Retry's `@Backoff`. As there is no equivalent, the annotation has been moved to Spring Kafka proper with the following improvements: * Harmonized name (`@BackOff` instead of `@Backoff`). * Revisited Javadoc. * Support for expression evaluation and `java.util.Duration` format. The creation of a `BackOff` instance from the annotation is now isolated in `BackOffFactory` and the relevant tests have been added. `RetryTopicConfigurationBuilder` is mostly backward-compatible but `uniformRandomBackoff` has been deprecated as we feel that its name does not convey what it actually does. `RetryingDeserializer` no longer offer a `RecoveryCallback` but an equivalent function that takes `RetryException` as an input. This contains the exceptions thrown as well as the number of retry attempts. The use of BinaryExceptionClassifier has been replaced by the newly introduced `ExceptionMatcher` that is a copy of the original algorithm with a polished API. With the migration done, we believe that further improvements can be made here: `@BackOff` oddly looks like Spring Framework's `@Retryable`. As a matter of a fact, the `maxAttempts` and `includes`/`excludes` from `@RetryableTopic` are touching the same concepts. One option would be to open up `@Retryable` so that it can be used in more case. Another area of improvement is that harmonization of BackOff as a term. It is named "Backoff" in several places, including in `@RetryableTopic`, and it would be nice if the concept had the same syntax everywhere. With Spring Retry being completely removed, this commit also removes the dependency and any further references to it. Harmonize BackOff term in code and documentation Some code style cleanup Signed-off-by: Stéphane Nicoll <[email protected]>
1 parent 67e3296 commit fde7d31

File tree

55 files changed

+1465
-531
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1465
-531
lines changed

build.gradle

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ ext {
7171
scalaVersion = '2.13'
7272
springBootVersion = '3.5.0' // docs module
7373
springDataVersion = '2025.1.0-SNAPSHOT'
74-
springRetryVersion = '2.0.12'
7574
springVersion = '7.0.0-SNAPSHOT'
7675

7776
idPrefix = 'kafka'
@@ -249,9 +248,6 @@ project ('spring-kafka') {
249248
api 'org.springframework:spring-context'
250249
api 'org.springframework:spring-messaging'
251250
api 'org.springframework:spring-tx'
252-
api ("org.springframework.retry:spring-retry:$springRetryVersion") {
253-
exclude group: 'org.springframework'
254-
}
255251
api "org.apache.kafka:kafka-clients:$kafkaVersion"
256252
api 'io.micrometer:micrometer-observation'
257253
optionalApi "org.apache.kafka:kafka-streams:$kafkaVersion"
@@ -322,7 +318,6 @@ project ('spring-kafka-test') {
322318
api "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion"
323319
api 'org.springframework:spring-context'
324320
api 'org.springframework:spring-test'
325-
api "org.springframework.retry:spring-retry:$springRetryVersion"
326321

327322
api "org.apache.kafka:kafka-clients:$kafkaVersion:test"
328323
api "org.apache.kafka:kafka-server:$kafkaVersion"

samples/sample-04/src/main/java/com/example/Application.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import org.springframework.kafka.annotation.RetryableTopic;
2727
import org.springframework.kafka.support.KafkaHeaders;
2828
import org.springframework.messaging.handler.annotation.Header;
29-
import org.springframework.retry.annotation.Backoff;
29+
import org.springframework.kafka.annotation.BackOff;
3030

3131
/**
3232
* Sample shows use of topic-based retry.
@@ -44,7 +44,7 @@ public static void main(String[] args) {
4444
SpringApplication.run(Application.class, args);
4545
}
4646

47-
@RetryableTopic(attempts = "5", backoff = @Backoff(delay = 2_000, maxDelay = 10_000, multiplier = 2))
47+
@RetryableTopic(attempts = "5", backOff = @BackOff(delay = 2_000, maxDelay = 10_000, multiplier = 2))
4848
@KafkaListener(id = "fooGroup", topics = "topic4")
4949
public void listen(String in, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
5050
@Header(KafkaHeaders.OFFSET) long offset) {

spring-kafka-docs/src/main/antora/modules/ROOT/pages/appendix/change-history.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,8 @@ See xref:kafka/serdes.adoc#error-handling-deserializer[Using `ErrorHandlingDeser
236236

237237
[[x31-retryable]]
238238
=== Retryable Topics
239-
Change suffix `-retry-5000` to `-retry` when `@RetryableTopic(backoff = @Backoff(delay = 5000), attempts = "2", fixedDelayTopicStrategy = FixedDelayStrategy.SINGLE_TOPIC)`.
240-
If you want to keep suffix `-retry-5000`, use `@RetryableTopic(backoff = @Backoff(delay = 5000), attempts = "2")`.
239+
Change suffix `-retry-5000` to `-retry` when `@RetryableTopic(backOff = @BackOff(delay = 5000), attempts = "2", fixedDelayTopicStrategy = FixedDelayStrategy.SINGLE_TOPIC)`.
240+
If you want to keep suffix `-retry-5000`, use `@RetryableTopic(backOff = @BackOff(delay = 5000), attempts = "2")`.
241241
See xref:retrytopic/topic-naming.adoc[Topic Naming] for more information.
242242

243243
[[x31-c]]

spring-kafka-docs/src/main/antora/modules/ROOT/pages/kafka/annotation-error-handling.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ The exceptions that are considered fatal, by default, are:
228228
since these exceptions are unlikely to be resolved on a retried delivery.
229229

230230
You can add more exception types to the not-retryable category, or completely replace the map of classified exceptions.
231-
See the Javadocs for `DefaultErrorHandler.addNotRetryableException()` and `DefaultErrorHandler.setClassifications()` for more information, as well as those for the `spring-retry` `BinaryExceptionClassifier`.
231+
See the Javadocs for `DefaultErrorHandler.addNotRetryableException()` and `DefaultErrorHandler.setClassifications()` for more information, as well as `ExceptionMatcher`.
232232

233233
Here is an example that adds `IllegalArgumentException` to the not-retryable exceptions:
234234

@@ -502,7 +502,7 @@ The exceptions that are considered fatal, by default, are:
502502
since these exceptions are unlikely to be resolved on a retried delivery.
503503

504504
You can add more exception types to the not-retryable category, or completely replace the map of classified exceptions.
505-
See the Javadocs for `DefaultAfterRollbackProcessor.setClassifications()` for more information, as well as those for the `spring-retry` `BinaryExceptionClassifier`.
505+
See the Javadocs for `DefaultAfterRollbackProcessor.setClassifications()` for more information, as well as `ExceptionMatcher`.
506506

507507
Here is an example that adds `IllegalArgumentException` to the not-retryable exceptions:
508508

spring-kafka-docs/src/main/antora/modules/ROOT/pages/kafka/serdes.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,9 @@ ConsumerFactory cf = new DefaultKafkaConsumerFactory(myConsumerConfigs,
400400
new RetryingDeserializer(myUnreliableValueDeserializer, retryTemplate));
401401
----
402402

403-
Starting with version `3.1.2`, a `RecoveryCallback` can be set on the `RetryingDeserializer` optionally.
403+
A recovery callback be set on the `RetryingDeserializer`, to return a fallback object if all retries are exhausted.
404404

405-
Refer to the https://github.com/spring-projects/spring-retry[spring-retry] project for configuration of the `RetryTemplate` with a retry policy, back off policy, etc.
405+
Refer to the https://github.com/spring-projects/spring-framework[Spring Framework] project for configuration of the `RetryTemplate` with a retry policy, back off, etc.
406406

407407

408408
[[messaging-message-conversion]]

spring-kafka-docs/src/main/antora/modules/ROOT/pages/retrytopic/accessing-delivery-attempts.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Starting with version 3.0.10, a convenient `KafkaMessageHeaderAccessor` is provi
1717

1818
[souce, java]
1919
----
20-
@RetryableTopic(backoff = @Backoff(...))
20+
@RetryableTopic(backOff = @BackOff(...))
2121
@KafkaListener(id = "dh1", topics = "dh1")
2222
void listen(Thing thing, KafkaMessageHeaderAccessor accessor) {
2323
...

spring-kafka-docs/src/main/antora/modules/ROOT/pages/retrytopic/features.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ It includes:
2020
[source, java]
2121
----
2222
@RetryableTopic(attempts = 5,
23-
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000))
23+
backOff = @BackOff(delay = 1000, multiplier = 2, maxDelay = 5000))
2424
@KafkaListener(topics = "my-annotated-topic")
2525
public void processMessage(MyPojo message) {
2626
// ... message processing
@@ -68,7 +68,7 @@ If that time is reached, the next time the consumer throws an exception the mess
6868

6969
[source, java]
7070
----
71-
@RetryableTopic(backoff = @Backoff(2_000), timeout = 5_000)
71+
@RetryableTopic(backOff = @BackOff(2_000), timeout = 5_000)
7272
@KafkaListener(topics = "my-annotated-topic")
7373
public void processMessage(MyPojo message) {
7474
// ... message processing

spring-kafka-docs/src/main/antora/modules/ROOT/pages/retrytopic/topic-naming.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ NOTE: The previous `FixedDelayStrategy` is now deprecated, and can be replaced b
7979

8080
[source, java]
8181
----
82-
@RetryableTopic(backoff = @Backoff(2_000), sameIntervalTopicReuseStrategy = SameIntervalTopicReuseStrategy.SINGLE_TOPIC)
82+
@RetryableTopic(backOff = @BackOff(2_000), sameIntervalTopicReuseStrategy = SameIntervalTopicReuseStrategy.SINGLE_TOPIC)
8383
@KafkaListener(topics = "my-annotated-topic")
8484
public void processMessage(MyPojo message) {
8585
// ... message processing
@@ -138,7 +138,7 @@ If multiple topics are required, then that can be done using the following confi
138138
[source, java]
139139
----
140140
@RetryableTopic(attempts = 230,
141-
backoff = @Backoff(delay = 1_000, multiplier = 2, maxDelay = 16_000),
141+
backOff = @BackOff(delay = 1_000, multiplier = 2, maxDelay = 16_000),
142142
sameIntervalTopicReuseStrategy = SameIntervalTopicReuseStrategy.MULTIPLE_TOPICS)
143143
@KafkaListener(topics = "my-annotated-topic")
144144
public void processMessage(MyPojo message) {
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright 2018-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.kafka.annotation;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.core.annotation.AliasFor;
26+
import org.springframework.core.retry.RetryPolicy;
27+
import org.springframework.format.annotation.DurationFormat;
28+
import org.springframework.util.backoff.ExponentialBackOff;
29+
import org.springframework.util.backoff.FixedBackOff;
30+
31+
/**
32+
* Collects metadata for creating a {@link org.springframework.util.backoff.BackOff BackOff}
33+
* instance as part of a {@link RetryPolicy}. Values can be provided as is or using a
34+
* {@code *String} equivalent that supports more format, as well as expression evaluations.
35+
* <p>
36+
* The available attributes lead to the following:
37+
* <ul>
38+
* <li>With no explicit settings, the default is a {@link FixedBackOff} with a delay of
39+
* {@value #DEFAULT_DELAY} ms</li>
40+
* <li>With only {@link #delay()} set: a fixed delay back off with that value</li>
41+
* <li>In all other cases, an {@link ExponentialBackOff} is created with the values of
42+
* {@link #delay()} (default: {@value RetryPolicy.Builder#DEFAULT_DELAY} ms),
43+
* {@link #maxDelay()} (default: no maximum), {@link #multiplier()}
44+
* (default: {@value RetryPolicy.Builder#DEFAULT_MULTIPLIER}) and {@link #jitter()}
45+
* (default: no jitter).</li>
46+
* </ul>
47+
*
48+
* @author Dave Syer
49+
* @author Gary Russell
50+
* @author Aftab Shaikh
51+
* @author Stephane Nicoll
52+
*
53+
* @since 4.0
54+
*/
55+
@Target(ElementType.ANNOTATION_TYPE)
56+
@Retention(RetentionPolicy.RUNTIME)
57+
@Documented
58+
public @interface BackOff {
59+
60+
/**
61+
* Default {@link #delay()} in milliseconds.
62+
*/
63+
long DEFAULT_DELAY = 1000;
64+
65+
/**
66+
* Alias for {@link #delay()}.
67+
* <p>Intended to be used when no other attributes are needed, for example:
68+
* {@code @BackOff(2000)}.
69+
*
70+
* @return the based delay in milliseconds (default{@value DEFAULT_DELAY})
71+
*/
72+
@AliasFor("delay")
73+
long value() default DEFAULT_DELAY;
74+
75+
/**
76+
* Specify the base delay after the initial invocation.
77+
* <p>If only a {@code delay} is specified, a {@link FixedBackOff} with that value
78+
* as the interval is configured.
79+
* <p>If a {@linkplain #multiplier() multiplier} is specified, this serves as the
80+
* initial delay to multiply from.
81+
* <p>The default is {@value DEFAULT_DELAY} milliseconds.
82+
*
83+
* @return the based delay in milliseconds (default{@value DEFAULT_DELAY})
84+
*/
85+
@AliasFor("value")
86+
long delay() default DEFAULT_DELAY;
87+
88+
/**
89+
* Specify the base delay after the initial invocation using a String format. If
90+
* this is specified, takes precedence over {@link #delay()}.
91+
* <p>The delay String can be in several formats:
92+
* <ul>
93+
* <li>a plain long &mdash; which is interpreted to represent a duration in
94+
* milliseconds</li>
95+
* <li>any of the known {@link DurationFormat.Style}: the {@link DurationFormat.Style#ISO8601 ISO8601}
96+
* style or the {@link DurationFormat.Style#SIMPLE SIMPLE} style &mdash; using
97+
* milliseconds as fallback if the string doesn't contain an explicit unit</li>
98+
* <li>Regular expressions, such as {@code ${example.property}} to use the
99+
* {@code example.property} from the environment</li>
100+
* </ul>
101+
*
102+
* @return the based delay as a String value &mdash; for example a placeholder
103+
* @see #delay()
104+
*/
105+
String delayString() default "";
106+
107+
/**
108+
* Specify the maximum delay for any retry attempt, limiting how far
109+
* {@linkplain #jitter jitter} and the {@linkplain #multiplier() multiplier} can
110+
* increase the {@linkplain #delay() delay}.
111+
* <p>Ignored if only {@link #delay()} is set, otherwise an {@link ExponentialBackOff}
112+
* with the given max delay or an unlimited delay if not set.
113+
*
114+
* @return the maximum delay
115+
*/
116+
long maxDelay() default 0;
117+
118+
/**
119+
* Specify the maximum delay for any retry attempt using a String format. If this is
120+
* specified, takes precedence over {@link #maxDelay()}..
121+
* <p>The max delay String can be in several formats:
122+
* <ul>
123+
* <li>a plain long &mdash; which is interpreted to represent a duration in
124+
* milliseconds</li>
125+
* <li>any of the known {@link DurationFormat.Style}: the {@link DurationFormat.Style#ISO8601 ISO8601}
126+
* style or the {@link DurationFormat.Style#SIMPLE SIMPLE} style &mdash; using
127+
* milliseconds as fallback if the string doesn't contain an explicit unit</li>
128+
* <li>Regular expressions, such as {@code ${example.property}} to use the
129+
* {@code example.property} from the environment</li>
130+
* </ul>
131+
*
132+
* @return the max delay as a String value &mdash; for example a placeholder
133+
* @see #maxDelay()
134+
*/
135+
String maxDelayString() default "";
136+
137+
/**
138+
* Specify a multiplier for a delay for the next retry attempt, applied to the previous
139+
* delay, starting with the initial {@linkplain #delay() delay} as well as to the
140+
* applicable {@linkplain #jitter() jitter} for each attempt.
141+
* <p>Ignored if only {@link #delay()} is set, otherwise an {@link ExponentialBackOff}
142+
* with the given multiplier or {@code 1.0} if not set.
143+
*
144+
* @return the value to multiply the current interval by for each attempt
145+
*/
146+
double multiplier() default 0;
147+
148+
/**
149+
* Specify a multiplier for a delay for the next retry attempt using a String format.
150+
* If this is specified, takes precedence over {@link #multiplier()}.
151+
* <p>The multiplier String can be in several formats:
152+
* <ul>
153+
* <li>a plain double</li>
154+
* <li>Regular expressions, such as {@code ${example.property}} to use the
155+
* {@code example.property} from the environment</li>
156+
* </ul>
157+
*
158+
* @return the value to multiply the current interval by for each attempt &mdash;
159+
* for example, a placeholder
160+
* @see #multiplier()
161+
*/
162+
String multiplierString() default "";
163+
164+
/**
165+
* Specify a jitter value for the base retry attempt, randomly subtracted or added to
166+
* the calculated delay, resulting in a value between {@code delay - jitter} and
167+
* {@code delay + jitter} but never below the {@linkplain #delay() base delay} or
168+
* above the {@linkplain #maxDelay() max delay}.
169+
* <p>If a {@linkplain #multiplier() multiplier} is specified, it is applied to the
170+
* jitter value as well.
171+
* <p>Ignored if only {@link #delay()} is set, otherwise an {@link ExponentialBackOff}
172+
* with the given jitter or no jitter if not set.
173+
*
174+
* @return the jitter value in milliseconds
175+
* @see #delay()
176+
* @see #maxDelay()
177+
* @see #multiplier()
178+
*/
179+
long jitter() default 0;
180+
181+
/**
182+
* Specify a jitter value for the base retry attempt using a String format. If this is
183+
* specified, takes precedence over {@link #jitter()}.
184+
* <p>The jitter String can be in several formats:
185+
* <ul>
186+
* <li>a plain long &mdash; which is interpreted to represent a duration in
187+
* milliseconds</li>
188+
* <li>any of the known {@link DurationFormat.Style}: the {@link DurationFormat.Style#ISO8601 ISO8601}
189+
* style or the {@link DurationFormat.Style#SIMPLE SIMPLE} style &mdash; using
190+
* milliseconds as fallback if the string doesn't contain an explicit unit</li>
191+
* <li>Regular expressions, such as {@code ${example.property}} to use the
192+
* {@code example.property} from the environment</li>
193+
* </ul>
194+
*
195+
* @return the jitter as a String value &mdash; for example, a placeholder
196+
* @see #jitter()
197+
*/
198+
String jitterString() default "";
199+
200+
}

0 commit comments

Comments
 (0)