16
16
17
17
package org .springframework .kafka .config ;
18
18
19
+ import java .time .Clock ;
19
20
import java .util .ArrayList ;
20
21
import java .util .List ;
21
22
import java .util .function .Consumer ;
26
27
import org .springframework .context .ApplicationContext ;
27
28
import org .springframework .context .annotation .Bean ;
28
29
import org .springframework .context .annotation .Configuration ;
30
+ import org .springframework .core .task .TaskExecutor ;
29
31
import org .springframework .kafka .annotation .EnableRetryTopic ;
30
32
import org .springframework .kafka .listener .CommonErrorHandler ;
31
33
import org .springframework .kafka .listener .ConcurrentMessageListenerContainer ;
32
34
import org .springframework .kafka .listener .DeadLetterPublishingRecoverer ;
35
+ import org .springframework .kafka .listener .DefaultErrorHandler ;
33
36
import org .springframework .kafka .listener .ExceptionClassifier ;
37
+ import org .springframework .kafka .listener .KafkaBackOffManagerFactory ;
34
38
import org .springframework .kafka .listener .KafkaConsumerBackoffManager ;
39
+ import org .springframework .kafka .listener .KafkaConsumerTimingAdjuster ;
35
40
import org .springframework .kafka .listener .ListenerContainerRegistry ;
36
41
import org .springframework .kafka .listener .MessageListenerContainer ;
37
42
import org .springframework .kafka .listener .PartitionPausingBackOffManagerFactory ;
43
+ import org .springframework .kafka .listener .WakingKafkaConsumerTimingAdjuster ;
38
44
import org .springframework .kafka .retrytopic .DeadLetterPublishingRecovererFactory ;
39
45
import org .springframework .kafka .retrytopic .DefaultDestinationTopicResolver ;
40
46
import org .springframework .kafka .retrytopic .DestinationTopicProcessor ;
46
52
import org .springframework .kafka .retrytopic .RetryTopicConfigurer ;
47
53
import org .springframework .kafka .retrytopic .RetryTopicInternalBeanNames ;
48
54
import org .springframework .kafka .retrytopic .RetryTopicNamesProviderFactory ;
55
+ import org .springframework .scheduling .concurrent .ThreadPoolTaskExecutor ;
49
56
import org .springframework .util .Assert ;
50
57
import org .springframework .util .backoff .BackOff ;
58
+ import org .springframework .util .backoff .FixedBackOff ;
51
59
52
60
/**
53
61
* This is the main class providing the configuration behind the non-blocking,
@@ -66,6 +74,9 @@ public class RetryTopicConfigurationSupport {
66
74
67
75
private final RetryTopicComponentFactory componentFactory = createComponentFactory ();
68
76
77
+ private static final String BACK_OFF_MANAGER_THREAD_EXECUTOR_BEAN_NAME = "backOffManagerThreadExecutor" ;
78
+ private static final int NOT_SET = -1 ;
79
+
69
80
/**
70
81
* Return a global {@link RetryTopicConfigurer} for configuring retry topics
71
82
* for {@link KafkaListenerEndpoint} instances with a corresponding
@@ -253,24 +264,79 @@ protected Consumer<DestinationTopicResolver> customizeDestinationTopicResolver()
253
264
}
254
265
255
266
/**
256
- * Provides the {@link KafkaConsumerBackoffManager} instance.
257
- * Override this method to provide a customized implementation.
258
- * A {@link PartitionPausingBackOffManagerFactory} can be used for that purpose,
259
- * or a different implementation can be provided.
267
+ * Create the {@link KafkaConsumerBackoffManager} instance that will be used to
268
+ * back off partitions.
269
+ * To configure it, override the {@link #configureKafkaBackOffManager} method.
270
+ * To provide a different implementation, either override this method, or
271
+ * override the {@link RetryTopicComponentFactory#kafkaBackOffManagerFactory} method
272
+ * and return a different {@link KafkaBackOffManagerFactory}.
260
273
* @param registry the {@link ListenerContainerRegistry} to be used to fetch the
261
274
* {@link MessageListenerContainer} to be backed off.
275
+ * @param taskExecutor the {@link TaskExecutor} to be used with the
276
+ * {@link KafkaConsumerTimingAdjuster}.
262
277
* @return the instance.
263
278
*/
264
279
@ Bean (name = KafkaListenerConfigUtils .KAFKA_CONSUMER_BACK_OFF_MANAGER_BEAN_NAME )
265
280
public KafkaConsumerBackoffManager kafkaConsumerBackoffManager (
266
281
@ Qualifier (KafkaListenerConfigUtils .KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME )
267
- ListenerContainerRegistry registry ) {
268
- return this .componentFactory .kafkaBackOffManagerFactory (registry ).create ();
282
+ ListenerContainerRegistry registry ,
283
+ @ Qualifier (BACK_OFF_MANAGER_THREAD_EXECUTOR_BEAN_NAME ) TaskExecutor taskExecutor ) {
284
+
285
+ KafkaBackOffManagerFactory backOffManagerFactory =
286
+ this .componentFactory .kafkaBackOffManagerFactory (registry );
287
+ if (backOffManagerFactory instanceof PartitionPausingBackOffManagerFactory ) {
288
+ configurePartitionPausingFactory (taskExecutor ,
289
+ (PartitionPausingBackOffManagerFactory ) backOffManagerFactory );
290
+ }
291
+ return backOffManagerFactory .create ();
292
+ }
293
+
294
+ private void configurePartitionPausingFactory (TaskExecutor taskExecutor ,
295
+ PartitionPausingBackOffManagerFactory factory ) {
296
+ KafkaBackOffManagerConfigurer configurer = new KafkaBackOffManagerConfigurer ();
297
+ configureKafkaBackOffManager (configurer );
298
+ factory .setTimingAdjustmentEnabled (configurer .timingAdjustmentEnabled );
299
+ if (configurer .timingAdjustmentEnabled ) {
300
+ factory .setTaskExecutor (taskExecutor );
301
+ if (configurer .maxThreadPoolSize != NOT_SET ) {
302
+ Assert .isInstanceOf (ThreadPoolTaskExecutor .class , taskExecutor ,
303
+ () -> "TaskExecutor must be an instance of ThreadPoolTaskExecutor to set maxThreadPoolSize" );
304
+ ((ThreadPoolTaskExecutor ) taskExecutor ).setMaxPoolSize (configurer .maxThreadPoolSize );
305
+ }
306
+ }
307
+ if (configurer .clock != null ) {
308
+ factory .setClock (configurer .clock );
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Create the {@link TaskExecutor} instance that will be used with
314
+ * the {@link WakingKafkaConsumerTimingAdjuster} if
315
+ * A {@link PartitionPausingBackOffManagerFactory} can be used for that purpose,
316
+ * or a different implementation can be provided.
317
+ * {@link MessageListenerContainer} to be backed off.
318
+ * @return the instance.
319
+ */
320
+ @ Bean (name = BACK_OFF_MANAGER_THREAD_EXECUTOR_BEAN_NAME )
321
+ public TaskExecutor backoffManagerTaskExecutor () {
322
+ KafkaBackOffManagerConfigurer configurer = new KafkaBackOffManagerConfigurer ();
323
+ configureKafkaBackOffManager (configurer );
324
+ return configurer .timingAdjustmentEnabled
325
+ ? this .componentFactory .taskExecutor ()
326
+ : task -> {
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Override this method to configure the {@link KafkaConsumerBackoffManager}.
332
+ * @param backOffManagerConfigurer a {@link KafkaBackOffManagerConfigurer}.
333
+ */
334
+ protected void configureKafkaBackOffManager (KafkaBackOffManagerConfigurer backOffManagerConfigurer ) {
269
335
}
270
336
271
337
/**
272
- * Override this method if you want to provide a subclass
273
- * of {@link RetryTopicComponentFactory} with different
338
+ * Override this method to provide a subclass of
339
+ * {@link RetryTopicComponentFactory} with different
274
340
* component implementations or subclasses.
275
341
* @return the instance.
276
342
*/
@@ -284,42 +350,132 @@ RetryTopicBootstrapper retryTopicBootstrapper(ApplicationContext context) {
284
350
return new RetryTopicBootstrapper (context , context .getAutowireCapableBeanFactory ());
285
351
}
286
352
353
+ /**
354
+ * Configure blocking retries to be used along non-blocking.
355
+ */
287
356
public static class BlockingRetriesConfigurer {
288
357
289
358
private BackOff backOff ;
290
359
291
360
private Class <? extends Exception >[] retryableExceptions ;
292
361
362
+ /**
363
+ * Set the exceptions that should be retried by the blocking retry mechanism.
364
+ * @param exceptions the exceptions.
365
+ * @return the configurer.
366
+ * @see DefaultErrorHandler
367
+ */
293
368
@ SuppressWarnings ("varargs" )
294
369
@ SafeVarargs
295
370
public final BlockingRetriesConfigurer retryOn (Class <? extends Exception >... exceptions ) {
296
371
this .retryableExceptions = exceptions ;
297
372
return this ;
298
373
}
299
374
375
+ /**
376
+ * Set the {@link BackOff} that should be used with the blocking retry mechanism.
377
+ * By default, a {@link FixedBackOff} with 0 delay and 9 retry attempts
378
+ * is configured. Note that this only has any effect for exceptions specified
379
+ * with the {@link #retryOn} method - by default blocking retries are disabled.
380
+ * @param backoff the {@link BackOff} instance.
381
+ * @return the configurer.
382
+ * @see DefaultErrorHandler
383
+ */
300
384
public BlockingRetriesConfigurer backOff (BackOff backoff ) {
301
385
this .backOff = backoff ;
302
386
return this ;
303
387
}
304
388
}
305
389
390
+ /**
391
+ * Configure the {@link KafkaConsumerBackoffManager} instance.
392
+ */
393
+ public static class KafkaBackOffManagerConfigurer {
394
+
395
+ boolean timingAdjustmentEnabled = true ;
396
+
397
+ private int maxThreadPoolSize = NOT_SET ;
398
+
399
+ private Clock clock ;
400
+
401
+ /**
402
+ * Disable timing adjustment for the delays. With this option records won't be
403
+ * processed exactly at the proper time. It's guaranteed however that records
404
+ * won't be processed before their due time.
405
+ * @return the configurer.
406
+ * @see WakingKafkaConsumerTimingAdjuster
407
+ */
408
+ public KafkaBackOffManagerConfigurer disableTimingAdjustment () {
409
+ this .timingAdjustmentEnabled = false ;
410
+ return this ;
411
+ }
412
+
413
+ /**
414
+ * Set the maximum thread pool size to be used by the
415
+ * {@link WakingKafkaConsumerTimingAdjuster}. This
416
+ * {@link KafkaConsumerTimingAdjuster} implementation spawns threads that will
417
+ * sleep for a calculated time, and after that will
418
+ * {@link org.apache.kafka.clients.consumer.Consumer#wakeup()} the consumer, in
419
+ * order to improve delay precision.
420
+ * @param maxThreadPoolSize the maximum thread pool size.
421
+ * @return the configurer.
422
+ */
423
+ public KafkaBackOffManagerConfigurer setMaxThreadPoolSize (int maxThreadPoolSize ) {
424
+ this .maxThreadPoolSize = maxThreadPoolSize ;
425
+ return this ;
426
+ }
427
+
428
+ /**
429
+ * Set the {@link Clock} instance to be used with the
430
+ * {@link KafkaConsumerBackoffManager}.
431
+ * @param clock the clock instance.
432
+ * @return the configurer.
433
+ */
434
+ public KafkaBackOffManagerConfigurer setClock (Clock clock ) {
435
+ this .clock = clock ;
436
+ return this ;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Configure customizers for components instantiated by the retry topics feature.
442
+ */
306
443
public static class CustomizersConfigurer {
307
444
308
445
private Consumer <CommonErrorHandler > errorHandlerCustomizer ;
309
446
private Consumer <ConcurrentMessageListenerContainer <?, ?>> listenerContainerCustomizer ;
310
447
private Consumer <DeadLetterPublishingRecoverer > deadLetterPublishingRecovererCustomizer ;
311
448
312
- protected CustomizersConfigurer customizeErrorHandler (Consumer <CommonErrorHandler > errorHandlerCustomizer ) {
449
+ /**
450
+ * Customize the {@link CommonErrorHandler} instances that will be used for the
451
+ * feature.
452
+ * @param errorHandlerCustomizer the customizer.
453
+ * @return the configurer.
454
+ * @see DefaultErrorHandler
455
+ */
456
+ public CustomizersConfigurer customizeErrorHandler (Consumer <CommonErrorHandler > errorHandlerCustomizer ) {
313
457
this .errorHandlerCustomizer = errorHandlerCustomizer ;
314
458
return this ;
315
459
}
316
460
317
- protected CustomizersConfigurer customizeListenerContainer (Consumer <ConcurrentMessageListenerContainer <?, ?>> listenerContainerCustomizer ) {
461
+ /**
462
+ * Customize the {@link ConcurrentMessageListenerContainer} instances created
463
+ * for the retry and DLT consumers.
464
+ * @param listenerContainerCustomizer the customizer.
465
+ * @return the configurer.
466
+ */
467
+ public CustomizersConfigurer customizeListenerContainer (Consumer <ConcurrentMessageListenerContainer <?, ?>> listenerContainerCustomizer ) {
318
468
this .listenerContainerCustomizer = listenerContainerCustomizer ;
319
469
return this ;
320
470
}
321
471
322
- protected CustomizersConfigurer customizeDeadLetterPublishingRecoverer (Consumer <DeadLetterPublishingRecoverer > dlprCustomizer ) {
472
+ /**
473
+ * Customize the {@link DeadLetterPublishingRecoverer} that will be used to
474
+ * forward the records to the retry topics and DLT.
475
+ * @param dlprCustomizer the customizer.
476
+ * @return the configurer.
477
+ */
478
+ public CustomizersConfigurer customizeDeadLetterPublishingRecoverer (Consumer <DeadLetterPublishingRecoverer > dlprCustomizer ) {
323
479
this .deadLetterPublishingRecovererCustomizer = dlprCustomizer ;
324
480
return this ;
325
481
}
0 commit comments