32
32
33
33
import org .springframework .kafka .core .KafkaOperations ;
34
34
import org .springframework .kafka .listener .ContainerProperties .EOSMode ;
35
+ import org .springframework .kafka .support .KafkaUtils ;
35
36
import org .springframework .lang .Nullable ;
36
37
import org .springframework .util .Assert ;
38
+ import org .springframework .util .CollectionUtils ;
37
39
import org .springframework .util .backoff .BackOff ;
38
40
import org .springframework .util .backoff .BackOffExecution ;
39
41
@@ -63,7 +65,9 @@ public class DefaultAfterRollbackProcessor<K, V> extends FailedBatchProcessor
63
65
64
66
private final BackOff backOff ;
65
67
66
- private KafkaOperations <?, ?> kafkaTemplate ;
68
+ private final KafkaOperations <?, ?> kafkaTemplate ;
69
+
70
+ private final BiConsumer <List <ConsumerRecord <?, ?>>, Exception > recoverer ;
67
71
68
72
/**
69
73
* Construct an instance with the default recoverer which simply logs the record after
@@ -140,20 +144,29 @@ public DefaultAfterRollbackProcessor(@Nullable
140
144
public DefaultAfterRollbackProcessor (@ Nullable BiConsumer <ConsumerRecord <?, ?>, Exception > recoverer ,
141
145
BackOff backOff , @ Nullable BackOffHandler backOffHandler , @ Nullable KafkaOperations <?, ?> kafkaOperations ,
142
146
boolean commitRecovered ) {
143
-
144
- super (recoverer , backOff , backOffHandler , null );
147
+ super (recoverer , backOff , backOffHandler , new CommonErrorHandler () { });
145
148
this .kafkaTemplate = kafkaOperations ;
146
149
super .setCommitRecovered (commitRecovered );
147
150
checkConfig ();
148
151
this .backOff = backOff ;
152
+ this .recoverer = (crs , ex ) -> {
153
+ if (!CollectionUtils .isEmpty (crs )) {
154
+ if (recoverer == null ) {
155
+ this .logger .error (ex , () -> "Records discarded: " + recordsToString (crs ));
156
+ }
157
+ else {
158
+ crs .spliterator ().forEachRemaining (rec -> recoverer .accept (rec , ex ));
159
+ }
160
+ }
161
+ };
149
162
}
150
163
151
164
private void checkConfig () {
152
165
Assert .isTrue (!isCommitRecovered () || this .kafkaTemplate != null ,
153
166
"A KafkaOperations is required when 'commitRecovered' is true" );
154
167
}
155
168
156
- @ SuppressWarnings ({ "unchecked" , "rawtypes" , "deprecation" })
169
+ @ SuppressWarnings ({ "unchecked" , "rawtypes" })
157
170
@ Override
158
171
public void process (List <ConsumerRecord <K , V >> records , Consumer <K , V > consumer ,
159
172
@ Nullable MessageListenerContainer container , Exception exception , boolean recoverable , EOSMode eosMode ) {
@@ -179,42 +192,52 @@ && isCommitRecovered() && this.kafkaTemplate.isTransactional()) {
179
192
180
193
}
181
194
195
+ @ SuppressWarnings ({ "unchecked" , "rawtypes" })
182
196
@ Override
183
197
public void processBatch (ConsumerRecords <K , V > records , List <ConsumerRecord <K , V >> recordList , Consumer <K , V > consumer ,
184
198
@ Nullable MessageListenerContainer container , Exception exception , boolean recoverable , EOSMode eosMode ) {
185
199
186
- int index = handlerBatchListenerFailedException (exception , records );
187
- if (index > -1 ) {
188
- List <ConsumerRecord <?, ?>> toCommit = new ArrayList <>();
189
- List <ConsumerRecord <?, ?>> remaining = new ArrayList <>();
190
- for (ConsumerRecord <?, ?> datum : records ) {
191
- if (index -- > 0 ) {
192
- toCommit .add (datum );
193
- }
194
- else {
195
- remaining .add (datum );
196
- }
200
+ long nextBackOff = ListenerUtils .nextBackOff (this .backOff , this .backOffs );
201
+ if (nextBackOff != BackOffExecution .STOP ) {
202
+ SeekUtils .doSeeks ((List ) recordList , consumer , exception , recoverable ,
203
+ getFailureTracker ()::recovered , container , this .logger );
204
+ try {
205
+ ListenerUtils .stoppableSleep (container , nextBackOff );
206
+ }
207
+ catch (InterruptedException e ) {
208
+ Thread .currentThread ().interrupt ();
209
+ }
210
+ return ;
211
+ }
212
+
213
+ int indexArg = handlerBatchListenerFailedException (exception , records );
214
+ int index = indexArg > -1 ? indexArg + 1 : recordList .size ();
215
+ List <ConsumerRecord <?, ?>> toRecover = new ArrayList <>();
216
+ List <ConsumerRecord <?, ?>> remaining = new ArrayList <>();
217
+ for (ConsumerRecord <?, ?> datum : records ) {
218
+ if (index -- > 0 ) {
219
+ toRecover .add (datum );
220
+ }
221
+ else {
222
+ remaining .add (datum );
197
223
}
224
+ }
225
+
226
+ try {
227
+ this .recoverer .accept (toRecover , exception );
228
+ SeekUtils .doSeeks (remaining , consumer , exception , recoverable ,
229
+ getFailureTracker ()::recovered , container , this .logger );
198
230
Map <TopicPartition , OffsetAndMetadata > offsets = new HashMap <>();
199
- toCommit .forEach (rec -> offsets .compute (new TopicPartition (rec .topic (), rec .partition ()),
231
+ toRecover .forEach (rec -> offsets .compute (new TopicPartition (rec .topic (), rec .partition ()),
200
232
(key , val ) -> ListenerUtils .createOffsetAndMetadata (container , rec .offset () + 1 )));
201
233
if (offsets .size () > 0 && this .kafkaTemplate != null && this .kafkaTemplate .isTransactional ()) {
202
234
this .kafkaTemplate .sendOffsetsToTransaction (offsets , consumer .groupMetadata ());
203
235
}
204
-
205
- // skip first record
206
- if (SeekUtils .doSeeks (remaining , consumer , exception , recoverable ,
207
- getFailureTracker ()::recovered , container , this .logger )
208
- && isCommitRecovered () && this .kafkaTemplate .isTransactional ()) {
209
- ConsumerRecord <?, ?> skipped = remaining .get (0 );
210
- this .kafkaTemplate .sendOffsetsToTransaction (
211
- Collections .singletonMap (new TopicPartition (skipped .topic (), skipped .partition ()),
212
- createOffsetAndMetadata (container , skipped .offset () + 1 )
213
- ), consumer .groupMetadata ());
214
- }
236
+ clearThreadState ();
215
237
}
216
- else {
217
- process (recordList , consumer , container , exception , false , eosMode );
238
+ catch (Exception ex ) {
239
+ process (recordList , consumer , container , exception , recoverable , eosMode );
240
+ logger .error (ex , () -> "Recoverer threw an exception; re-seeking batch" );
218
241
}
219
242
220
243
}
@@ -238,4 +261,14 @@ private static OffsetAndMetadata createOffsetAndMetadata(@Nullable MessageListen
238
261
}
239
262
return ListenerUtils .createOffsetAndMetadata (container , offset );
240
263
}
264
+
265
+ private static String recordsToString (List <ConsumerRecord <?, ?>> records ) {
266
+ StringBuffer sb = new StringBuffer ();
267
+ records .spliterator ().forEachRemaining (rec -> sb
268
+ .append (KafkaUtils .format (rec ))
269
+ .append (',' ));
270
+ sb .deleteCharAt (sb .length () - 1 );
271
+ return sb .toString ();
272
+ }
273
+
241
274
}
0 commit comments