26
26
import com .arpnetworking .steno .LoggerFactory ;
27
27
import net .sf .oval .constraint .CheckWith ;
28
28
import net .sf .oval .constraint .CheckWithCheck ;
29
+ import net .sf .oval .constraint .Min ;
29
30
import net .sf .oval .constraint .NotNull ;
30
31
import org .apache .kafka .clients .consumer .Consumer ;
31
32
import org .apache .kafka .clients .consumer .ConsumerRecord ;
32
33
import org .apache .kafka .common .KafkaException ;
33
34
34
35
import java .time .Duration ;
36
+ import java .util .concurrent .ArrayBlockingQueue ;
37
+ import java .util .concurrent .BlockingQueue ;
35
38
import java .util .concurrent .ExecutorService ;
36
39
import java .util .concurrent .Executors ;
37
40
import java .util .concurrent .TimeUnit ;
38
41
39
42
/**
40
- * Produce instances of {@code Record} from the values of entries
43
+ * Produce instances of {@link com.arpnetworking.metrics.mad.model. Record} from the values of entries
41
44
* from a Kafka topic. The key from the entries gets discarded
42
45
*
43
46
* @param <T> the type of data created by the source
@@ -52,28 +55,47 @@ public final class KafkaSource<T, V> extends BaseSource {
52
55
private final Consumer <?, V > _consumer ;
53
56
private final RunnableConsumer _runnableConsumer ;
54
57
private final ExecutorService _consumerExecutor ;
58
+ private final ExecutorService _parserExecutor ;
55
59
private final Parser <T , V > _parser ;
56
60
private final Logger _logger ;
57
61
private final Duration _shutdownAwaitTime ;
58
62
private final Duration _backoffTime ;
63
+ private final Integer _numWorkerThreads ;
64
+ private final BlockingQueue <V > _buffer ;
65
+ private final ParsingWorker _parsingWorker = new ParsingWorker ();
59
66
60
67
@ Override
61
68
public void start () {
62
69
_consumerExecutor .execute (_runnableConsumer );
70
+ for (int i = 0 ; i < _numWorkerThreads ; i ++) {
71
+ _parserExecutor .execute (_parsingWorker );
72
+ }
63
73
}
64
74
65
75
@ Override
66
76
public void stop () {
67
77
_runnableConsumer .stop ();
68
-
69
78
_consumerExecutor .shutdown ();
70
79
try {
71
80
_consumerExecutor .awaitTermination (_shutdownAwaitTime .toMillis (), TimeUnit .MILLISECONDS );
72
81
} catch (final InterruptedException e ) {
73
- LOGGER .warn ()
82
+ _logger .warn ()
74
83
.setMessage ("Unable to shutdown kafka consumer executor" )
75
84
.setThrowable (e )
76
85
.log ();
86
+ } finally {
87
+ _consumer .close ();
88
+ }
89
+
90
+ _parsingWorker .stop ();
91
+ _parserExecutor .shutdown ();
92
+ try {
93
+ _parserExecutor .awaitTermination (_shutdownAwaitTime .toMillis (), TimeUnit .MILLISECONDS );
94
+ } catch (final InterruptedException e ) {
95
+ _logger .warn ()
96
+ .setMessage ("Unable to shutdown parsing worker executor" )
97
+ .setThrowable (e )
98
+ .log ();
77
99
}
78
100
}
79
101
@@ -97,39 +119,88 @@ public String toString() {
97
119
98
120
@ SuppressWarnings ("unused" )
99
121
private KafkaSource (final Builder <T , V > builder ) {
100
- this (builder , LOGGER );
122
+ this (builder , LOGGER , new ArrayBlockingQueue <>( builder . _bufferSize ) );
101
123
}
102
124
125
+ // NOTE: Package private for testing
103
126
/* package private */ KafkaSource (final Builder <T , V > builder , final Logger logger ) {
127
+ this (builder , logger , new ArrayBlockingQueue <>(builder ._bufferSize ));
128
+ }
129
+
130
+ // NOTE: Package private for testing
131
+ /* package private */ KafkaSource (final Builder <T , V > builder , final BlockingQueue <V > buffer ) {
132
+ this (builder , LOGGER , buffer );
133
+ }
134
+
135
+ private KafkaSource (final Builder <T , V > builder , final Logger logger , final BlockingQueue <V > buffer ) {
104
136
super (builder );
105
- _logger = logger ;
106
137
_consumer = builder ._consumer ;
107
138
_parser = builder ._parser ;
108
139
_runnableConsumer = new RunnableConsumerImpl .Builder <V >()
109
140
.setConsumer (builder ._consumer )
110
141
.setListener (new LogConsumerListener ())
111
142
.setPollTime (builder ._pollTime )
112
143
.build ();
144
+ _numWorkerThreads = builder ._numWorkerThreads ;
113
145
_consumerExecutor = Executors .newSingleThreadExecutor (runnable -> new Thread (runnable , "KafkaConsumer" ));
146
+ _parserExecutor = Executors .newFixedThreadPool (_numWorkerThreads );
114
147
_shutdownAwaitTime = builder ._shutdownAwaitTime ;
115
148
_backoffTime = builder ._backoffTime ;
149
+ _logger = logger ;
150
+ _buffer = buffer ;
151
+ }
152
+
153
+ private class ParsingWorker implements Runnable {
154
+ private volatile boolean _isRunning = true ;
155
+
156
+ @ Override
157
+ public void run () {
158
+ while (_isRunning || !_buffer .isEmpty ()) { // Empty the queue before stopping the workers
159
+ final V value = _buffer .poll ();
160
+ if (value != null ) {
161
+ final T record ;
162
+ try {
163
+ record = _parser .parse (value );
164
+ } catch (final ParsingException e ) {
165
+ _logger .error ()
166
+ .setMessage ("Failed to parse data" )
167
+ .setThrowable (e )
168
+ .log ();
169
+ return ;
170
+ }
171
+ KafkaSource .this .notify (record );
172
+ } else {
173
+ // Queue is empty
174
+ try {
175
+ Thread .sleep (_backoffTime .toMillis ());
176
+ } catch (final InterruptedException e ) {
177
+ Thread .currentThread ().interrupt ();
178
+ stop ();
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ public void stop () {
185
+ _isRunning = false ;
186
+ }
116
187
}
117
188
118
189
private class LogConsumerListener implements ConsumerListener <V > {
119
190
120
191
@ Override
121
192
public void handle (final ConsumerRecord <?, V > consumerRecord ) {
122
- final T record ;
123
193
try {
124
- record = _parser .parse (consumerRecord .value ());
125
- } catch (final ParsingException e ) {
126
- _logger .error ()
127
- .setMessage ("Failed to parse data" )
194
+ _buffer .put (consumerRecord .value ());
195
+ } catch (final InterruptedException e ) {
196
+ _logger .info ()
197
+ .setMessage ("Consumer thread interrupted" )
198
+ .addData ("source" , KafkaSource .this )
199
+ .addData ("action" , "stopping" )
128
200
.setThrowable (e )
129
201
.log ();
130
- return ;
202
+ _runnableConsumer . stop () ;
131
203
}
132
- KafkaSource .this .notify (record );
133
204
}
134
205
135
206
@ Override
@@ -181,7 +252,7 @@ private void backoff(final Throwable throwable) {
181
252
}
182
253
183
254
/**
184
- * Builder pattern class for {@code KafkaSource}.
255
+ * Builder pattern class for {@link KafkaSource}.
185
256
*
186
257
* @param <T> the type of data created by the source
187
258
* @param <V> the type of data of value in kafka {@code ConsumerRecords}
@@ -231,11 +302,11 @@ public Builder<T, V> setPollTime(final Duration pollTime) {
231
302
}
232
303
233
304
/**
234
- * Sets the amount of time the {@link KafkaSource} will wait to shutdown the {@code RunnableConsumer} thread.
305
+ * Sets the amount of time the {@link KafkaSource} will wait to shutdown the {@link RunnableConsumer} thread.
235
306
* Default is 10 seconds. Cannot be null or negative.
236
307
*
237
308
* @param shutdownAwaitTime The {@code Duration} the {@link KafkaSource} will wait to shutdown
238
- * the {@code RunnableConsumer} thread.
309
+ * the {@link RunnableConsumer} thread.
239
310
* @return This instance of {@link KafkaSource.Builder}.
240
311
*/
241
312
public Builder <T , V > setShutdownAwaitTime (final Duration shutdownAwaitTime ) {
@@ -256,6 +327,30 @@ public Builder<T, V> setBackoffTime(final Duration backoffTime) {
256
327
return this ;
257
328
}
258
329
330
+ /**
331
+ * Sets the number of threads that will parse {@code ConsumerRecord}s from the {@code Consumer}.
332
+ * Default is 1. Must be greater than or equal to 1.
333
+ *
334
+ * @param numWorkerThreads The number of parsing worker threads.
335
+ * @return This instance of {@link KafkaSource.Builder}.
336
+ */
337
+ public Builder <T , V > setNumWorkerThreads (final Integer numWorkerThreads ) {
338
+ _numWorkerThreads = numWorkerThreads ;
339
+ return this ;
340
+ }
341
+
342
+ /**
343
+ * Sets the size of the buffer to hold {@code ConsumerRecord}s from the {@code Consumer} before they are parsed.
344
+ * Default is 1000. Must be greater than or equal to 1.
345
+ *
346
+ * @param bufferSize The size of the buffer.
347
+ * @return This instance of {@link KafkaSource.Builder}.
348
+ */
349
+ public Builder <T , V > setBufferSize (final Integer bufferSize ) {
350
+ _bufferSize = bufferSize ;
351
+ return this ;
352
+ }
353
+
259
354
@ Override
260
355
protected Builder <T , V > self () {
261
356
return this ;
@@ -274,6 +369,12 @@ protected Builder<T, V> self() {
274
369
@ NotNull
275
370
@ CheckWith (value = PositiveDuration .class , message = "Backoff time must be positive." )
276
371
private Duration _backoffTime = Duration .ofSeconds (1 );
372
+ @ NotNull
373
+ @ Min (1 )
374
+ private Integer _numWorkerThreads = 1 ;
375
+ @ NotNull
376
+ @ Min (1 )
377
+ private Integer _bufferSize = 1000 ;
277
378
278
379
private static class PositiveDuration implements CheckWithCheck .SimpleCheck {
279
380
@ Override
0 commit comments