|
16 | 16 |
|
17 | 17 | package org.springframework.kafka.listener; |
18 | 18 |
|
19 | | -import java.time.Clock; |
20 | | -import java.time.Instant; |
21 | | -import java.util.HashMap; |
22 | | -import java.util.Map; |
23 | | - |
24 | | -import org.apache.commons.logging.LogFactory; |
25 | 19 | import org.apache.kafka.clients.consumer.Consumer; |
26 | 20 | import org.apache.kafka.common.TopicPartition; |
27 | 21 |
|
28 | | -import org.springframework.context.ApplicationListener; |
29 | | -import org.springframework.core.log.LogAccessor; |
30 | | -import org.springframework.kafka.event.ListenerContainerPartitionIdleEvent; |
31 | 22 | import org.springframework.lang.Nullable; |
32 | 23 |
|
33 | 24 | /** |
34 | | - * |
35 | | - * A manager that backs off consumption for a given topic if the timestamp provided is not |
36 | | - * due. Use with {@link SeekToCurrentErrorHandler} to guarantee that the message is read |
37 | | - * again after partition consumption is resumed (or seek it manually by other means). |
38 | | - * It's also necessary to set a {@link ContainerProperties#setIdlePartitionEventInterval(Long)} |
39 | | - * so the Manager can resume the partition consumption. |
40 | | - * |
41 | | - * Note that when a record backs off the partition consumption gets paused for |
42 | | - * approximately that amount of time, so you must have a fixed backoff value per partition. |
| 25 | + * Interface for backing off a {@link MessageListenerContainer} |
| 26 | + * until a given dueTimestamp, if such timestamp is in the future. |
43 | 27 | * |
44 | 28 | * @author Tomaz Fernandes |
45 | 29 | * @author Gary Russell |
46 | 30 | * @since 2.7 |
47 | | - * @see SeekToCurrentErrorHandler |
48 | 31 | */ |
49 | | -public class KafkaConsumerBackoffManager implements ApplicationListener<ListenerContainerPartitionIdleEvent> { |
50 | | - |
51 | | - private static final LogAccessor LOGGER = new LogAccessor(LogFactory.getLog(KafkaConsumerBackoffManager.class)); |
52 | | - |
53 | | - private final ListenerContainerRegistry listenerContainerRegistry; |
| 32 | +public interface KafkaConsumerBackoffManager { |
54 | 33 |
|
55 | | - private final Map<TopicPartition, Context> backOffContexts; |
56 | | - |
57 | | - private final Clock clock; |
58 | | - |
59 | | - private final KafkaConsumerTimingAdjuster kafkaConsumerTimingAdjuster; |
60 | | - |
61 | | - /** |
62 | | - * Constructs an instance with the provided {@link ListenerContainerRegistry} and |
63 | | - * {@link KafkaConsumerTimingAdjuster}. |
64 | | - * |
65 | | - * The ListenerContainerRegistry is used to fetch the {@link MessageListenerContainer} |
66 | | - * that will be backed off / resumed. |
67 | | - * |
68 | | - * The KafkaConsumerTimingAdjuster is used to make timing adjustments |
69 | | - * in the message consumption so that it processes the message closer |
70 | | - * to its due time rather than later. |
71 | | - * |
72 | | - * @param listenerContainerRegistry the listenerContainerRegistry to use. |
73 | | - * @param kafkaConsumerTimingAdjuster the kafkaConsumerTimingAdjuster to use. |
74 | | - */ |
75 | | - public KafkaConsumerBackoffManager(ListenerContainerRegistry listenerContainerRegistry, |
76 | | - KafkaConsumerTimingAdjuster kafkaConsumerTimingAdjuster) { |
| 34 | + void backOffIfNecessary(Context context); |
77 | 35 |
|
78 | | - this.listenerContainerRegistry = listenerContainerRegistry; |
79 | | - this.kafkaConsumerTimingAdjuster = kafkaConsumerTimingAdjuster; |
80 | | - this.clock = Clock.systemUTC(); |
81 | | - this.backOffContexts = new HashMap<>(); |
82 | | - } |
83 | | - |
84 | | - /** |
85 | | - * Constructs an instance with the provided {@link ListenerContainerRegistry} |
86 | | - * and with no timing adjustment capabilities. |
87 | | - * |
88 | | - * The ListenerContainerRegistry is used to fetch the {@link MessageListenerContainer} |
89 | | - * that will be backed off / resumed. |
90 | | - * |
91 | | - * @param listenerContainerRegistry the listenerContainerRegistry to use. |
92 | | - */ |
93 | | - public KafkaConsumerBackoffManager(ListenerContainerRegistry listenerContainerRegistry) { |
94 | | - |
95 | | - this.listenerContainerRegistry = listenerContainerRegistry; |
96 | | - this.kafkaConsumerTimingAdjuster = null; |
97 | | - this.clock = Clock.systemUTC(); |
98 | | - this.backOffContexts = new HashMap<>(); |
99 | | - } |
100 | | - |
101 | | - /** |
102 | | - * Creates an instance with the provided {@link ListenerContainerRegistry}, |
103 | | - * {@link KafkaConsumerTimingAdjuster} and {@link Clock}. |
104 | | - * |
105 | | - * @param listenerContainerRegistry the listenerContainerRegistry to use. |
106 | | - * @param kafkaConsumerTimingAdjuster the kafkaConsumerTimingAdjuster to use. |
107 | | - * @param clock the clock to use. |
108 | | - */ |
109 | | - public KafkaConsumerBackoffManager(ListenerContainerRegistry listenerContainerRegistry, |
110 | | - KafkaConsumerTimingAdjuster kafkaConsumerTimingAdjuster, |
111 | | - Clock clock) { |
112 | | - |
113 | | - this.listenerContainerRegistry = listenerContainerRegistry; |
114 | | - this.clock = clock; |
115 | | - this.kafkaConsumerTimingAdjuster = kafkaConsumerTimingAdjuster; |
116 | | - this.backOffContexts = new HashMap<>(); |
117 | | - } |
118 | | - |
119 | | - /** |
120 | | - * Creates an instance with the provided {@link ListenerContainerRegistry} |
121 | | - * and {@link Clock}, with no timing adjustment capabilities. |
122 | | - * |
123 | | - * @param listenerContainerRegistry the listenerContainerRegistry to use. |
124 | | - * @param clock the clock to use. |
125 | | - */ |
126 | | - public KafkaConsumerBackoffManager(ListenerContainerRegistry listenerContainerRegistry, Clock clock) { |
127 | | - |
128 | | - this.listenerContainerRegistry = listenerContainerRegistry; |
129 | | - this.clock = clock; |
130 | | - this.kafkaConsumerTimingAdjuster = null; |
131 | | - this.backOffContexts = new HashMap<>(); |
132 | | - } |
133 | | - |
134 | | - /** |
135 | | - * Backs off if the current time is before the dueTimestamp provided |
136 | | - * in the {@link Context} object. |
137 | | - * @param context the back off context for this execution. |
138 | | - */ |
139 | | - public void maybeBackoff(Context context) { |
140 | | - long backoffTime = context.dueTimestamp - getCurrentMillisFromClock(); |
141 | | - if (backoffTime > 0) { |
142 | | - pauseConsumptionAndThrow(context, backoffTime); |
143 | | - } |
144 | | - } |
145 | | - |
146 | | - private void pauseConsumptionAndThrow(Context context, Long backOffTime) throws KafkaBackoffException { |
147 | | - TopicPartition topicPartition = context.topicPartition; |
148 | | - getListenerContainerFromContext(context).pausePartition(topicPartition); |
149 | | - addBackoff(context, topicPartition); |
150 | | - throw new KafkaBackoffException(String.format("Partition %s from topic %s is not ready for consumption, " + |
151 | | - "backing off for approx. %s millis.", context.topicPartition.partition(), |
152 | | - context.topicPartition.topic(), backOffTime), |
153 | | - topicPartition, context.listenerId, context.dueTimestamp); |
154 | | - } |
155 | | - |
156 | | - @Override |
157 | | - public void onApplicationEvent(ListenerContainerPartitionIdleEvent partitionIdleEvent) { |
158 | | - LOGGER.debug(() -> String.format("partitionIdleEvent received at %s. Partition: %s", |
159 | | - getCurrentMillisFromClock(), partitionIdleEvent.getTopicPartition())); |
160 | | - |
161 | | - Context backOffContext = getBackOffContext(partitionIdleEvent.getTopicPartition()); |
162 | | - maybeResumeConsumption(backOffContext); |
163 | | - } |
164 | | - |
165 | | - private long getCurrentMillisFromClock() { |
166 | | - return Instant.now(this.clock).toEpochMilli(); |
167 | | - } |
168 | | - |
169 | | - private void maybeResumeConsumption(@Nullable Context context) { |
170 | | - if (context == null) { |
171 | | - return; |
172 | | - } |
173 | | - long now = getCurrentMillisFromClock(); |
174 | | - long timeUntilDue = context.dueTimestamp - now; |
175 | | - long pollTimeout = getListenerContainerFromContext(context) |
176 | | - .getContainerProperties() |
177 | | - .getPollTimeout(); |
178 | | - boolean isDue = timeUntilDue <= pollTimeout; |
179 | | - |
180 | | - long adjustedAmount = applyTimingAdjustment(context, timeUntilDue, pollTimeout); |
181 | | - |
182 | | - if (adjustedAmount != 0L || isDue) { |
183 | | - resumePartition(context); |
184 | | - } |
185 | | - else { |
186 | | - LOGGER.debug(() -> String.format("TopicPartition %s not due. DueTimestamp: %s Now: %s ", |
187 | | - context.topicPartition, context.dueTimestamp, now)); |
188 | | - } |
189 | | - } |
190 | | - |
191 | | - private long applyTimingAdjustment(Context context, long timeUntilDue, long pollTimeout) { |
192 | | - if (this.kafkaConsumerTimingAdjuster == null || context.consumerForTimingAdjustment == null) { |
193 | | - LOGGER.debug(() -> String.format( |
194 | | - "Skipping timing adjustment for TopicPartition %s.", context.topicPartition)); |
195 | | - return 0L; |
196 | | - } |
197 | | - return this.kafkaConsumerTimingAdjuster.adjustTiming( |
198 | | - context.consumerForTimingAdjustment, context.topicPartition, pollTimeout, timeUntilDue); |
199 | | - } |
200 | | - |
201 | | - private void resumePartition(Context context) { |
202 | | - MessageListenerContainer container = getListenerContainerFromContext(context); |
203 | | - LOGGER.debug(() -> "Resuming partition at " + getCurrentMillisFromClock()); |
204 | | - container.resumePartition(context.topicPartition); |
205 | | - removeBackoff(context.topicPartition); |
206 | | - } |
207 | | - |
208 | | - private MessageListenerContainer getListenerContainerFromContext(Context context) { |
209 | | - return this.listenerContainerRegistry.getListenerContainer(context.listenerId); |
210 | | - } |
211 | | - |
212 | | - protected void addBackoff(Context context, TopicPartition topicPartition) { |
213 | | - synchronized (this.backOffContexts) { |
214 | | - this.backOffContexts.put(topicPartition, context); |
215 | | - } |
216 | | - } |
217 | | - |
218 | | - protected @Nullable Context getBackOffContext(TopicPartition topicPartition) { |
219 | | - synchronized (this.backOffContexts) { |
220 | | - return this.backOffContexts.get(topicPartition); |
221 | | - } |
222 | | - } |
223 | | - |
224 | | - protected void removeBackoff(TopicPartition topicPartition) { |
225 | | - synchronized (this.backOffContexts) { |
226 | | - this.backOffContexts.remove(topicPartition); |
227 | | - } |
228 | | - } |
229 | | - |
230 | | - public Context createContext(long dueTimestamp, String listenerId, TopicPartition topicPartition, |
231 | | - @Nullable Consumer<?, ?> consumerForTimingAdjustment) { |
232 | | - return new Context(dueTimestamp, topicPartition, listenerId, consumerForTimingAdjustment); |
| 36 | + default Context createContext(long dueTimestamp, String listenerId, TopicPartition topicPartition, |
| 37 | + @Nullable Consumer<?, ?> messageConsumer) { |
| 38 | + return new Context(dueTimestamp, topicPartition, listenerId, messageConsumer); |
233 | 39 | } |
234 | 40 |
|
235 | 41 | /** |
236 | 42 | * Provides the state that will be used for backing off. |
237 | 43 | * @since 2.7 |
238 | 44 | */ |
239 | | - public static class Context { |
| 45 | + class Context { |
240 | 46 |
|
241 | 47 | /** |
242 | 48 | * The time after which the message should be processed, |
243 | 49 | * in milliseconds since epoch. |
244 | 50 | */ |
245 | | - private final long dueTimestamp; // NOSONAR |
| 51 | + private final long dueTimestamp; |
246 | 52 |
|
247 | 53 | /** |
248 | 54 | * The id for the listener that should be paused. |
249 | 55 | */ |
250 | | - private final String listenerId; // NOSONAR |
| 56 | + private final String listenerId; |
251 | 57 |
|
252 | 58 | /** |
253 | 59 | * The topic that contains the partition to be paused. |
254 | 60 | */ |
255 | | - private final TopicPartition topicPartition; // NOSONAR |
| 61 | + private final TopicPartition topicPartition; |
256 | 62 |
|
257 | 63 | /** |
258 | 64 | * The consumer of the message, if present. |
259 | 65 | */ |
260 | | - private final Consumer<?, ?> consumerForTimingAdjustment; // NOSONAR |
| 66 | + private final Consumer<?, ?> consumerForTimingAdjustment; |
261 | 67 |
|
262 | 68 | Context(long dueTimestamp, TopicPartition topicPartition, String listenerId, |
263 | | - @Nullable Consumer<?, ?> consumerForTimingAdjustment) { |
| 69 | + @Nullable Consumer<?, ?> consumerForTimingAdjustment) { |
| 70 | + |
264 | 71 | this.dueTimestamp = dueTimestamp; |
265 | 72 | this.listenerId = listenerId; |
266 | 73 | this.topicPartition = topicPartition; |
267 | 74 | this.consumerForTimingAdjustment = consumerForTimingAdjustment; |
268 | 75 | } |
| 76 | + |
| 77 | + public long getDueTimestamp() { |
| 78 | + return this.dueTimestamp; |
| 79 | + } |
| 80 | + |
| 81 | + public String getListenerId() { |
| 82 | + return this.listenerId; |
| 83 | + } |
| 84 | + |
| 85 | + public TopicPartition getTopicPartition() { |
| 86 | + return this.topicPartition; |
| 87 | + } |
| 88 | + |
| 89 | + public @Nullable Consumer<?, ?> getConsumerForTimingAdjustment() { |
| 90 | + return this.consumerForTimingAdjustment; |
| 91 | + } |
| 92 | + |
269 | 93 | } |
| 94 | + |
270 | 95 | } |
0 commit comments