logstash-integration-kafka 12.1.11-java → 12.2.0-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/docs/input-kafka.asciidoc +164 -12
- data/lib/logstash/inputs/kafka.rb +227 -46
- data/lib/logstash-integration-kafka_jars.rb +1 -1
- data/spec/integration/inputs/kafka_spec.rb +91 -0
- data/spec/unit/inputs/kafka_spec.rb +220 -0
- data/vendor/jar-dependencies/at/yawk/lz4/lz4-java/{1.12.0/lz4-java-1.12.0.jar → 1.12.1/lz4-java-1.12.1.jar} +0 -0
- data/version +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ce12663a941ab9311aaa94446325a942ee2c8ee8d3f76b3b0a510b2c7729297
|
|
4
|
+
data.tar.gz: 70fa9a7637f015f1f0a53b7273151643bddaf8c22451a24bfa1ff309052ccbb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a5dad3768d4295bc13b4739c758efb3f141b009d2342086ef8ee4e130bc943c1c7285fde656624b5aaa1e4c05f5ed92ae6b6386983c34aee4bb018d4f0e7e04
|
|
7
|
+
data.tar.gz: 75b92286c2c12e1d49ec0b86d33a50ede5697e830e09b531bc68c458f0f62a0e686f2143a7e952150ea58eba52e56e4f838f4db36aa488b94bd826f52679692c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
## 12.2.0
|
|
2
|
+
- Add share group consumer support to Kafka input plugin via `consumer_mode => "share_group"` (Kafka 4.0+ KIP-932) [#252](https://github.com/logstash-plugins/logstash-integration-kafka/pull/252) [#252](https://github.com/logstash-plugins/logstash-integration-kafka/pull/252)
|
|
3
|
+
|
|
1
4
|
## 12.1.11
|
|
2
5
|
- Update jackson dependency to 2.21.7, and require `snappy-java` 1.1.10.10 or later and `guava` 33.7.2-jre or later [#289](https://github.com/logstash-plugins/logstash-integration-kafka/pull/289)
|
|
3
6
|
|
data/docs/input-kafka.asciidoc
CHANGED
|
@@ -53,13 +53,48 @@ The Logstash Kafka consumer handles group management and uses the default offset
|
|
|
53
53
|
strategy using Kafka topics.
|
|
54
54
|
|
|
55
55
|
Logstash instances by default form a single logical group to subscribe to Kafka topics
|
|
56
|
-
Each Logstash Kafka consumer can run multiple threads to increase read throughput. Alternatively,
|
|
56
|
+
Each Logstash Kafka consumer can run multiple threads to increase read throughput. Alternatively,
|
|
57
57
|
you could run multiple Logstash instances with the same `group_id` to spread the load across
|
|
58
58
|
physical machines. Messages in a topic will be distributed to all Logstash instances with
|
|
59
59
|
the same `group_id`.
|
|
60
60
|
|
|
61
61
|
Ideally you should have as many threads as the number of partitions for a perfect balance --
|
|
62
|
-
more threads than partitions means that some threads will be idle
|
|
62
|
+
more threads than partitions means that some threads will be idle. This applies to the default
|
|
63
|
+
`consumer_mode => consumer_group`; it does not apply to `consumer_mode => share_group`, which is
|
|
64
|
+
not partition-oriented. See <<plugins-{type}s-{plugin}-consumer_threads>>.
|
|
65
|
+
|
|
66
|
+
[id="plugins-{type}s-{plugin}-share-groups"]
|
|
67
|
+
==== Kafka Share Groups (queue semantics)
|
|
68
|
+
|
|
69
|
+
Setting <<plugins-{type}s-{plugin}-consumer_mode>> to `share_group` enables Kafka 4.0+ Share
|
|
70
|
+
Groups (KIP-932). Share groups provide *queue semantics*: any consumer in the group can read
|
|
71
|
+
from any partition, and each record is delivered to exactly one consumer. Unlike consumer
|
|
72
|
+
groups, the number of active consumers is no longer bounded by the partition count.
|
|
73
|
+
|
|
74
|
+
Requirements:
|
|
75
|
+
|
|
76
|
+
* Kafka brokers 4.0 or later with `group.share.enable=true`
|
|
77
|
+
* The `group_id` setting identifies the share group name
|
|
78
|
+
|
|
79
|
+
Share group consumers must acknowledge each record.
|
|
80
|
+
After an event is accepted by the pipeline's queue, it is acknowledged as `ACCEPT` to ensure that no other consumer in the share group receives it.
|
|
81
|
+
When an event is either _not_ accepted by the pipeline's queue or an error occurs before it is offered to the queue, the <<plugins-{type}s-{plugin}-share_group_acknowledgement_on_error>> option controls how the event is acknowledged (`release` for redelivery or `reject` to permanently discard).
|
|
82
|
+
|
|
83
|
+
Example configuration:
|
|
84
|
+
|
|
85
|
+
[source,ruby]
|
|
86
|
+
--
|
|
87
|
+
input {
|
|
88
|
+
kafka {
|
|
89
|
+
bootstrap_servers => "localhost:9092"
|
|
90
|
+
topics => ["my-topic"]
|
|
91
|
+
group_id => "my-share-group"
|
|
92
|
+
consumer_mode => "share_group"
|
|
93
|
+
consumer_threads => 4
|
|
94
|
+
share_group_acknowledgement_on_error => "release"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
--
|
|
63
98
|
|
|
64
99
|
For more information see https://kafka.apache.org/{kafka_client_doc}/documentation.html#theconsumer
|
|
65
100
|
|
|
@@ -123,6 +158,7 @@ See the https://kafka.apache.org/{kafka_client_doc}/documentation for more detai
|
|
|
123
158
|
| <<plugins-{type}s-{plugin}-client_id>> |<<string,string>>|No
|
|
124
159
|
| <<plugins-{type}s-{plugin}-client_rack>> |<<string,string>>|No
|
|
125
160
|
| <<plugins-{type}s-{plugin}-connections_max_idle_ms>> |<<number,number>>|No
|
|
161
|
+
| <<plugins-{type}s-{plugin}-consumer_mode>> |<<string,string>>, one of `["consumer_group", "share_group"]`|No
|
|
126
162
|
| <<plugins-{type}s-{plugin}-consumer_threads>> |<<number,number>>|No
|
|
127
163
|
| <<plugins-{type}s-{plugin}-decorate_events>> |<<string,string>>|No
|
|
128
164
|
| <<plugins-{type}s-{plugin}-enable_auto_commit>> |<<boolean,boolean>>|No
|
|
@@ -175,6 +211,7 @@ See the https://kafka.apache.org/{kafka_client_doc}/documentation for more detai
|
|
|
175
211
|
| <<plugins-{type}s-{plugin}-security_protocol>> |<<string,string>>, one of `["PLAINTEXT", "SSL", "SASL_PLAINTEXT", "SASL_SSL"]`|No
|
|
176
212
|
| <<plugins-{type}s-{plugin}-send_buffer_bytes>> |<<number,number>>|No
|
|
177
213
|
| <<plugins-{type}s-{plugin}-session_timeout_ms>> |<<number,number>>|No
|
|
214
|
+
| <<plugins-{type}s-{plugin}-share_group_acknowledgement_on_error>> |<<string,string>>, one of `["release", "reject"]`|No
|
|
178
215
|
| <<plugins-{type}s-{plugin}-ssl_endpoint_identification_algorithm>> |<<string,string>>|No
|
|
179
216
|
| <<plugins-{type}s-{plugin}-ssl_key_password>> |<<password,password>>|No
|
|
180
217
|
| <<plugins-{type}s-{plugin}-ssl_keystore_location>> |a valid filesystem path|No
|
|
@@ -194,13 +231,18 @@ input plugins.
|
|
|
194
231
|
|
|
195
232
|
|
|
196
233
|
[id="plugins-{type}s-{plugin}-auto_commit_interval_ms"]
|
|
197
|
-
===== `auto_commit_interval_ms`
|
|
234
|
+
===== `auto_commit_interval_ms`
|
|
198
235
|
|
|
199
236
|
* Value type is <<number,number>>
|
|
200
237
|
* Default value is `5000`.
|
|
201
238
|
|
|
202
239
|
The frequency in milliseconds that the consumer offsets are committed to Kafka.
|
|
203
240
|
|
|
241
|
+
[NOTE]
|
|
242
|
+
=====
|
|
243
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
244
|
+
=====
|
|
245
|
+
|
|
204
246
|
[id="plugins-{type}s-{plugin}-auto_offset_reset"]
|
|
205
247
|
===== `auto_offset_reset`
|
|
206
248
|
|
|
@@ -215,6 +257,12 @@ What to do when there is no initial offset in Kafka or if an offset is out of ra
|
|
|
215
257
|
* none: throw exception to the consumer if no previous offset is found for the consumer's group
|
|
216
258
|
* anything else: throw exception to the consumer.
|
|
217
259
|
|
|
260
|
+
[NOTE]
|
|
261
|
+
=====
|
|
262
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
263
|
+
For `share_group` mode, the equivalent is the broker/group-level `share.auto.offset.reset` setting.
|
|
264
|
+
=====
|
|
265
|
+
|
|
218
266
|
[id="plugins-{type}s-{plugin}-bootstrap_servers"]
|
|
219
267
|
===== `bootstrap_servers`
|
|
220
268
|
|
|
@@ -285,14 +333,53 @@ https://cwiki.apache.org/confluence/display/KAFKA/KIP-392%3A+Allow+consumers+to+
|
|
|
285
333
|
|
|
286
334
|
Close idle connections after the number of milliseconds specified by this config.
|
|
287
335
|
|
|
336
|
+
[id="plugins-{type}s-{plugin}-consumer_mode"]
|
|
337
|
+
===== `consumer_mode`
|
|
338
|
+
|
|
339
|
+
* Value can be either of: `consumer_group`, `share_group`
|
|
340
|
+
* Default value is `consumer_group`
|
|
341
|
+
|
|
342
|
+
Sets the consumer mode for this input.
|
|
343
|
+
|
|
344
|
+
`consumer_group` (default) uses the standard Kafka consumer group protocol (`KafkaConsumer`).
|
|
345
|
+
Partitions are assigned exclusively to consumers — each partition is consumed by exactly one
|
|
346
|
+
thread in the group at a time.
|
|
347
|
+
|
|
348
|
+
`share_group` uses the Kafka 4.0+ Share Group protocol (`KafkaShareConsumer`, KIP-932).
|
|
349
|
+
Share groups provide *queue semantics*: records from any partition can be fetched by any
|
|
350
|
+
consumer in the group, and each record is delivered to exactly one consumer. There is no
|
|
351
|
+
partition assignment, so any number of consumers can scale independently without being
|
|
352
|
+
limited by the partition count.
|
|
353
|
+
|
|
354
|
+
To use share groups, your Kafka brokers must be version 4.0 or later and must have
|
|
355
|
+
`group.share.enable=true` set in the broker configuration.
|
|
356
|
+
|
|
357
|
+
[NOTE]
|
|
358
|
+
====
|
|
359
|
+
Setting any of the following options in the pipeline config raises a configuration error in
|
|
360
|
+
`share_group` mode, since they do not apply to the Share Group protocol: `group_protocol`,
|
|
361
|
+
`group_instance_id`, `partition_assignment_strategy`, `heartbeat_interval_ms`,
|
|
362
|
+
`session_timeout_ms`, `isolation_level`, `enable_auto_commit`, `auto_commit_interval_ms`,
|
|
363
|
+
`auto_offset_reset`, `exclude_internal_topics`, `schema_registry_url`.
|
|
364
|
+
|
|
365
|
+
Some of these have broker/group-level equivalents for share groups that can be configured
|
|
366
|
+
directly on the broker (for example, `auto.offset.reset` → `share.auto.offset.reset`,
|
|
367
|
+
`heartbeat.interval.ms` → `share.heartbeat.interval.ms`,
|
|
368
|
+
`session.timeout.ms` → `share.session.timeout.ms`,
|
|
369
|
+
`isolation.level` → `share.isolation.level`).
|
|
370
|
+
====
|
|
371
|
+
|
|
288
372
|
[id="plugins-{type}s-{plugin}-consumer_threads"]
|
|
289
|
-
===== `consumer_threads`
|
|
373
|
+
===== `consumer_threads`
|
|
290
374
|
|
|
291
375
|
* Value type is <<number,number>>
|
|
292
376
|
* Default value is `1`
|
|
293
377
|
|
|
294
|
-
|
|
295
|
-
|
|
378
|
+
The number of consumer threads this input should spawn for fetching events and pushing them into the pipeline's queue.
|
|
379
|
+
|
|
380
|
+
When configured with `consumer_mode => consumer_group`, each partition in the topic can only be assigned to a single consumer at a time, so if the _total_ number of consumer threads across all consumers in the group exceeds the number of partitions, some of those threads will be idle.
|
|
381
|
+
|
|
382
|
+
This constraint doesn't apply when configured with `consumer_mode => share_group` which is not partition-oriented.
|
|
296
383
|
|
|
297
384
|
[id="plugins-{type}s-{plugin}-decorate_events"]
|
|
298
385
|
===== `decorate_events`
|
|
@@ -327,7 +414,7 @@ A topic will be auto-created only if this configuration is set to `true` and a
|
|
|
327
414
|
otherwise auto-topic creation is not permitted.
|
|
328
415
|
|
|
329
416
|
[id="plugins-{type}s-{plugin}-enable_auto_commit"]
|
|
330
|
-
===== `enable_auto_commit`
|
|
417
|
+
===== `enable_auto_commit`
|
|
331
418
|
|
|
332
419
|
* Value type is <<boolean,boolean>>
|
|
333
420
|
* Default value is `true`
|
|
@@ -339,8 +426,14 @@ If true, periodically commit to Kafka the offsets of messages already returned b
|
|
|
339
426
|
the consumer. If value is `false` however, the offset is committed every time the
|
|
340
427
|
consumer writes data fetched from the topic to the in-memory or persistent queue.
|
|
341
428
|
|
|
429
|
+
[NOTE]
|
|
430
|
+
=====
|
|
431
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
432
|
+
Share groups manage acknowledgement per record rather than via committed offsets — see <<plugins-{type}s-{plugin}-share_group_acknowledgement_on_error>>.
|
|
433
|
+
=====
|
|
434
|
+
|
|
342
435
|
[id="plugins-{type}s-{plugin}-exclude_internal_topics"]
|
|
343
|
-
===== `exclude_internal_topics`
|
|
436
|
+
===== `exclude_internal_topics`
|
|
344
437
|
|
|
345
438
|
* Value type is <<string,string>>
|
|
346
439
|
* There is no default value for this setting.
|
|
@@ -348,6 +441,11 @@ consumer writes data fetched from the topic to the in-memory or persistent queue
|
|
|
348
441
|
Whether records from internal topics (such as offsets) should be exposed to the consumer.
|
|
349
442
|
If set to true the only way to receive records from an internal topic is subscribing to it.
|
|
350
443
|
|
|
444
|
+
[NOTE]
|
|
445
|
+
=====
|
|
446
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
447
|
+
=====
|
|
448
|
+
|
|
351
449
|
[id="plugins-{type}s-{plugin}-fetch_max_bytes"]
|
|
352
450
|
===== `fetch_max_bytes`
|
|
353
451
|
|
|
@@ -414,6 +512,11 @@ You can set this value to use information such as a hostname, an IP, or anything
|
|
|
414
512
|
NOTE: In cases when multiple threads are configured and `consumer_threads` is greater than one, a suffix is appended to
|
|
415
513
|
the `group_instance_id` to avoid collisions.
|
|
416
514
|
|
|
515
|
+
[NOTE]
|
|
516
|
+
=====
|
|
517
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
518
|
+
=====
|
|
519
|
+
|
|
417
520
|
[id="plugins-{type}s-{plugin}-group_protocol"]
|
|
418
521
|
===== `group_protocol`
|
|
419
522
|
|
|
@@ -428,21 +531,31 @@ Specifies the consumer group rebalance protocol used by the Kafka client.
|
|
|
428
531
|
When using `consumer`, the following settings **cannot be configured**:
|
|
429
532
|
`partition_assignment_strategy`, `heartbeat_interval_ms`, and `session_timeout_ms`.
|
|
430
533
|
|
|
534
|
+
[NOTE]
|
|
535
|
+
=====
|
|
536
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
537
|
+
=====
|
|
431
538
|
|
|
432
539
|
[id="plugins-{type}s-{plugin}-heartbeat_interval_ms"]
|
|
433
|
-
===== `heartbeat_interval_ms`
|
|
540
|
+
===== `heartbeat_interval_ms`
|
|
434
541
|
|
|
435
542
|
* Value type is <<number,number>>
|
|
436
543
|
* Default value is `3000` milliseconds (3 seconds).
|
|
437
544
|
|
|
438
|
-
The expected time between heartbeats to the consumer coordinator. Heartbeats are used to ensure
|
|
545
|
+
The expected time between heartbeats to the consumer coordinator. Heartbeats are used to ensure
|
|
439
546
|
that the consumer's session stays active and to facilitate rebalancing when new
|
|
440
547
|
consumers join or leave the group. The value must be set lower than
|
|
441
548
|
`session.timeout.ms`, but typically should be set no higher than 1/3 of that value.
|
|
442
549
|
It can be adjusted even lower to control the expected time for normal rebalances.
|
|
443
550
|
|
|
551
|
+
[NOTE]
|
|
552
|
+
=====
|
|
553
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
554
|
+
For `share_group` mode, the equivalent is the broker/group-level `share.heartbeat.interval.ms` setting.
|
|
555
|
+
=====
|
|
556
|
+
|
|
444
557
|
[id="plugins-{type}s-{plugin}-isolation_level"]
|
|
445
|
-
===== `isolation_level`
|
|
558
|
+
===== `isolation_level`
|
|
446
559
|
|
|
447
560
|
* Value type is <<string,string>>
|
|
448
561
|
* Default value is `"read_uncommitted"`
|
|
@@ -452,6 +565,12 @@ transactional messages which have been committed. If set to `read_uncommitted` (
|
|
|
452
565
|
return all messages, even transactional messages which have been aborted. Non-transactional messages will be returned
|
|
453
566
|
unconditionally in either mode.
|
|
454
567
|
|
|
568
|
+
[NOTE]
|
|
569
|
+
=====
|
|
570
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
571
|
+
For `share_group` mode, the equivalent is the broker/group-level `share.isolation.level` setting.
|
|
572
|
+
=====
|
|
573
|
+
|
|
455
574
|
[id="plugins-{type}s-{plugin}-jaas_path"]
|
|
456
575
|
===== `jaas_path`
|
|
457
576
|
|
|
@@ -548,6 +667,12 @@ partition ownership amongst consumer instances, supported options are:
|
|
|
548
667
|
These map to Kafka's corresponding https://kafka.apache.org/{kafka_client_doc}/javadoc/org/apache/kafka/clients/consumer/ConsumerPartitionAssignor.html[`ConsumerPartitionAssignor`]
|
|
549
668
|
implementations.
|
|
550
669
|
|
|
670
|
+
[NOTE]
|
|
671
|
+
=====
|
|
672
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
673
|
+
Share groups do not assign partitions exclusively to consumers, so partition assignment strategies do not apply.
|
|
674
|
+
=====
|
|
675
|
+
|
|
551
676
|
[id="plugins-{type}s-{plugin}-poll_timeout_ms"]
|
|
552
677
|
===== `poll_timeout_ms`
|
|
553
678
|
|
|
@@ -796,6 +921,11 @@ The schemas must follow a naming convention with the pattern <topic name>-value.
|
|
|
796
921
|
Use either the Schema Registry config option or the
|
|
797
922
|
<<plugins-{type}s-{plugin}-value_deserializer_class>> config option, but not both.
|
|
798
923
|
|
|
924
|
+
[NOTE]
|
|
925
|
+
=====
|
|
926
|
+
Schema Registry options (including `schema_registry_key`, `schema_registry_secret`, `schema_registry_proxy`, `schema_registry_validation`, and `schema_registry_ssl_*`) are supported only with `consumer_mode => consumer_group`.
|
|
927
|
+
=====
|
|
928
|
+
|
|
799
929
|
[id="plugins-{type}s-{plugin}-schema_registry_validation"]
|
|
800
930
|
===== `schema_registry_validation`
|
|
801
931
|
|
|
@@ -825,7 +955,7 @@ Security protocol to use, which can be either of PLAINTEXT,SSL,SASL_PLAINTEXT,SA
|
|
|
825
955
|
The size of the TCP send buffer (SO_SNDBUF) to use when sending data
|
|
826
956
|
|
|
827
957
|
[id="plugins-{type}s-{plugin}-session_timeout_ms"]
|
|
828
|
-
===== `session_timeout_ms`
|
|
958
|
+
===== `session_timeout_ms`
|
|
829
959
|
|
|
830
960
|
* Value type is <<number,number>>
|
|
831
961
|
* Default value is `10000` milliseconds (10 seconds).
|
|
@@ -833,6 +963,28 @@ The size of the TCP send buffer (SO_SNDBUF) to use when sending data
|
|
|
833
963
|
The timeout after which, if the `poll_timeout_ms` is not invoked, the consumer is marked dead
|
|
834
964
|
and a rebalance operation is triggered for the group identified by `group_id`
|
|
835
965
|
|
|
966
|
+
[NOTE]
|
|
967
|
+
=====
|
|
968
|
+
This option is supported only with `consumer_mode => consumer_group`.
|
|
969
|
+
For `share_group` mode, the equivalent is the broker/group-level `share.session.timeout.ms` setting.
|
|
970
|
+
=====
|
|
971
|
+
|
|
972
|
+
[id="plugins-{type}s-{plugin}-share_group_acknowledgement_on_error"]
|
|
973
|
+
===== `share_group_acknowledgement_on_error`
|
|
974
|
+
|
|
975
|
+
* Value can be either of: `release`, `reject`
|
|
976
|
+
* Default value is `release`
|
|
977
|
+
|
|
978
|
+
Controls how a record is acknowledged when the event is either not accepted by the pipeline's queue or an error occurs before it is offered.
|
|
979
|
+
|
|
980
|
+
* `release` (default): The record is released back to the share group for redelivery to any available consumer. This strategy favors consistency over availability, which can allow a single malformed event to be retried indefinitely.
|
|
981
|
+
* `reject`: The record is permanently discarded and will not be redelivered. This strategy favors availability over consistency and risks data loss.
|
|
982
|
+
|
|
983
|
+
[NOTE]
|
|
984
|
+
=====
|
|
985
|
+
This option is supported only with `consumer_mode => share_group`.
|
|
986
|
+
=====
|
|
987
|
+
|
|
836
988
|
[id="plugins-{type}s-{plugin}-ssl_endpoint_identification_algorithm"]
|
|
837
989
|
===== `ssl_endpoint_identification_algorithm`
|
|
838
990
|
|
|
@@ -45,8 +45,10 @@ require 'logstash/plugin_mixins/deprecation_logger_support'
|
|
|
45
45
|
# physical machines. Messages in a topic will be distributed to all Logstash instances with
|
|
46
46
|
# the same `group_id`.
|
|
47
47
|
#
|
|
48
|
-
#
|
|
49
|
-
# more threads than partitions means that some threads will be idle
|
|
48
|
+
# In `consumer_group` mode, you should ideally have as many threads as the number of partitions
|
|
49
|
+
# for a perfect balance -- more threads than partitions means that some threads will be idle.
|
|
50
|
+
# This does not apply in `share_group` mode, where any thread can fetch from any partition, so
|
|
51
|
+
# the number of threads is not bounded by the partition count.
|
|
50
52
|
#
|
|
51
53
|
# For more information see http://kafka.apache.org/documentation.html#theconsumer
|
|
52
54
|
#
|
|
@@ -100,8 +102,10 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
|
|
|
100
102
|
# is to be able to track the source of requests beyond just ip/port by allowing
|
|
101
103
|
# a logical application name to be included.
|
|
102
104
|
config :client_id, :validate => :string, :default => "logstash"
|
|
103
|
-
#
|
|
104
|
-
# balance — more threads than partitions means that some threads will be idle
|
|
105
|
+
# In `consumer_group` mode, you should ideally have as many threads as the number of partitions
|
|
106
|
+
# for a perfect balance — more threads than partitions means that some threads will be idle.
|
|
107
|
+
# This does not apply in `share_group` mode, where any thread can fetch from any partition, so
|
|
108
|
+
# the number of threads is not bounded by the partition count.
|
|
105
109
|
config :consumer_threads, :validate => :number, :default => 1
|
|
106
110
|
# If true, periodically commit to Kafka the offsets of messages already returned by the consumer.
|
|
107
111
|
# This committed offset will be used when the process fails as the position from
|
|
@@ -281,6 +285,33 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
|
|
|
281
285
|
# otherwise auto-topic creation is not permitted.
|
|
282
286
|
config :auto_create_topics, :validate => :boolean, :default => true
|
|
283
287
|
|
|
288
|
+
# Sets the consumer mode for this input.
|
|
289
|
+
#
|
|
290
|
+
# * `consumer_group` (default): Standard Kafka consumer group mode. Partitions are assigned
|
|
291
|
+
# exclusively to consumers; each partition is consumed by exactly one thread in the group.
|
|
292
|
+
# This mode uses `KafkaConsumer` internally.
|
|
293
|
+
#
|
|
294
|
+
# * `share_group`: Kafka 4.0+ Share Group mode (KIP-932). Provides queue semantics where
|
|
295
|
+
# records from any partition can be delivered to any consumer in the share group, and each
|
|
296
|
+
# record is delivered to exactly one consumer. Consumers must acknowledge each record.
|
|
297
|
+
# This mode uses `KafkaShareConsumer` internally and requires Kafka brokers 4.0 or later
|
|
298
|
+
# with `group.share.enable=true` on the broker.
|
|
299
|
+
#
|
|
300
|
+
# The following options are not supported in `share_group` mode and raise a configuration
|
|
301
|
+
# error if set in the pipeline config: `group_protocol`, `group_instance_id`,
|
|
302
|
+
# `partition_assignment_strategy`, `heartbeat_interval_ms`, `session_timeout_ms`,
|
|
303
|
+
# `isolation_level`, `enable_auto_commit`, `auto_commit_interval_ms`, `auto_offset_reset`,
|
|
304
|
+
# `exclude_internal_topics`, `schema_registry_url`.
|
|
305
|
+
config :consumer_mode, :validate => ["consumer_group", "share_group"], :default => "consumer_group"
|
|
306
|
+
|
|
307
|
+
# Controls how records are acknowledged when `consumer_mode` is set to `share_group`.
|
|
308
|
+
# Successfully processed records are always acknowledged as ACCEPT.
|
|
309
|
+
# This setting controls what happens when an error occurs during record processing:
|
|
310
|
+
#
|
|
311
|
+
# * `release` (default): Re-queue the record for redelivery to any consumer in the share group.
|
|
312
|
+
# * `reject`: Permanently discard the record (it will not be redelivered).
|
|
313
|
+
config :share_group_acknowledgement_on_error, :validate => ["release", "reject"], :default => "release"
|
|
314
|
+
|
|
284
315
|
config :decorate_events, :validate => %w(none basic extended false true), :default => "none"
|
|
285
316
|
|
|
286
317
|
attr_reader :metadata_mode
|
|
@@ -294,15 +325,45 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
|
|
|
294
325
|
super(params)
|
|
295
326
|
end
|
|
296
327
|
|
|
328
|
+
SHARE_GROUP_INCOMPATIBLE_OPTIONS = %w[
|
|
329
|
+
group_protocol
|
|
330
|
+
group_instance_id
|
|
331
|
+
partition_assignment_strategy
|
|
332
|
+
heartbeat_interval_ms
|
|
333
|
+
session_timeout_ms
|
|
334
|
+
isolation_level
|
|
335
|
+
enable_auto_commit
|
|
336
|
+
auto_commit_interval_ms
|
|
337
|
+
auto_offset_reset
|
|
338
|
+
exclude_internal_topics
|
|
339
|
+
schema_registry_url
|
|
340
|
+
].freeze
|
|
341
|
+
|
|
342
|
+
CONSUMER_GROUP_ONLY_OPTIONS = %w[
|
|
343
|
+
share_group_acknowledgement_on_error
|
|
344
|
+
].freeze
|
|
345
|
+
|
|
297
346
|
public
|
|
298
347
|
def register
|
|
299
348
|
@runner_threads = []
|
|
300
349
|
@metadata_mode = extract_metadata_level(@decorate_events)
|
|
301
350
|
reassign_dns_lookup
|
|
302
351
|
@pattern ||= java.util.regex.Pattern.compile(@topics_pattern) unless @topics_pattern.nil?
|
|
303
|
-
check_schema_registry_parameters
|
|
304
352
|
|
|
305
|
-
|
|
353
|
+
if @consumer_mode == "share_group"
|
|
354
|
+
validate_share_group_config!
|
|
355
|
+
ack_type_class = org.apache.kafka.clients.consumer.AcknowledgeType
|
|
356
|
+
@share_group_ack_error_type =
|
|
357
|
+
if @share_group_acknowledgement_on_error == "reject"
|
|
358
|
+
ack_type_class::REJECT
|
|
359
|
+
else
|
|
360
|
+
ack_type_class::RELEASE
|
|
361
|
+
end
|
|
362
|
+
else
|
|
363
|
+
validate_consumer_group_config!
|
|
364
|
+
check_schema_registry_parameters
|
|
365
|
+
set_group_protocol!
|
|
366
|
+
end
|
|
306
367
|
end
|
|
307
368
|
|
|
308
369
|
METADATA_NONE = Set[].freeze
|
|
@@ -329,20 +390,35 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
|
|
|
329
390
|
|
|
330
391
|
public
|
|
331
392
|
def run(logstash_queue)
|
|
332
|
-
@
|
|
333
|
-
|
|
334
|
-
|
|
393
|
+
if @consumer_mode == "share_group"
|
|
394
|
+
@runner_consumers = consumer_threads.times.map do |i|
|
|
395
|
+
subscribe(create_share_consumer("#{client_id}-#{i}"))
|
|
396
|
+
end
|
|
397
|
+
@runner_threads = @runner_consumers.map.with_index do |consumer, i|
|
|
398
|
+
share_group_thread_runner(logstash_queue, consumer, "kafka-input-worker-#{client_id}-#{i}")
|
|
399
|
+
end
|
|
400
|
+
else
|
|
401
|
+
@runner_consumers = consumer_threads.times.map do |i|
|
|
402
|
+
thread_group_instance_id = consumer_threads > 1 && group_instance_id ? "#{group_instance_id}-#{i}" : group_instance_id
|
|
403
|
+
subscribe(create_consumer("#{client_id}-#{i}", thread_group_instance_id))
|
|
404
|
+
end
|
|
405
|
+
@runner_threads = @runner_consumers.map.with_index { |consumer, i| thread_runner(logstash_queue, consumer,
|
|
406
|
+
"kafka-input-worker-#{client_id}-#{i}") }
|
|
335
407
|
end
|
|
336
|
-
@runner_threads = @runner_consumers.map.with_index { |consumer, i| thread_runner(logstash_queue, consumer,
|
|
337
|
-
"kafka-input-worker-#{client_id}-#{i}") }
|
|
338
408
|
@runner_threads.each(&:start)
|
|
339
409
|
@runner_threads.each(&:join)
|
|
340
410
|
end # def run
|
|
341
411
|
|
|
342
412
|
public
|
|
343
413
|
def stop
|
|
344
|
-
#
|
|
345
|
-
@runner_consumers && @runner_consumers.each
|
|
414
|
+
# wake up consumers to unblock the poll loop in runner threads
|
|
415
|
+
@runner_consumers && @runner_consumers.each do |consumer|
|
|
416
|
+
begin
|
|
417
|
+
consumer.wakeup
|
|
418
|
+
rescue => e
|
|
419
|
+
logger.debug("Error waking up consumer on stop", :error => e.message)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
346
422
|
end
|
|
347
423
|
|
|
348
424
|
public
|
|
@@ -440,46 +516,63 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
|
|
|
440
516
|
end
|
|
441
517
|
|
|
442
518
|
private
|
|
519
|
+
def build_common_props(client_id)
|
|
520
|
+
props = java.util.Properties.new
|
|
521
|
+
kafka = org.apache.kafka.clients.consumer.ConsumerConfig
|
|
522
|
+
|
|
523
|
+
props.put(kafka::BOOTSTRAP_SERVERS_CONFIG, bootstrap_servers)
|
|
524
|
+
props.put(kafka::CLIENT_DNS_LOOKUP_CONFIG, client_dns_lookup)
|
|
525
|
+
props.put(kafka::CLIENT_ID_CONFIG, client_id)
|
|
526
|
+
props.put(kafka::CONNECTIONS_MAX_IDLE_MS_CONFIG, connections_max_idle_ms.to_s) unless connections_max_idle_ms.nil?
|
|
527
|
+
props.put(kafka::FETCH_MAX_BYTES_CONFIG, fetch_max_bytes.to_s) unless fetch_max_bytes.nil?
|
|
528
|
+
props.put(kafka::FETCH_MAX_WAIT_MS_CONFIG, fetch_max_wait_ms.to_s) unless fetch_max_wait_ms.nil?
|
|
529
|
+
props.put(kafka::FETCH_MIN_BYTES_CONFIG, fetch_min_bytes.to_s) unless fetch_min_bytes.nil?
|
|
530
|
+
props.put(kafka::GROUP_ID_CONFIG, group_id)
|
|
531
|
+
props.put(kafka::KEY_DESERIALIZER_CLASS_CONFIG, key_deserializer_class)
|
|
532
|
+
props.put(kafka::MAX_PARTITION_FETCH_BYTES_CONFIG, max_partition_fetch_bytes.to_s) unless max_partition_fetch_bytes.nil?
|
|
533
|
+
props.put(kafka::MAX_POLL_RECORDS_CONFIG, max_poll_records.to_s) unless max_poll_records.nil?
|
|
534
|
+
props.put(kafka::MAX_POLL_INTERVAL_MS_CONFIG, max_poll_interval_ms.to_s) unless max_poll_interval_ms.nil?
|
|
535
|
+
props.put(kafka::METADATA_MAX_AGE_CONFIG, metadata_max_age_ms.to_s) unless metadata_max_age_ms.nil?
|
|
536
|
+
props.put(kafka::RECEIVE_BUFFER_CONFIG, receive_buffer_bytes.to_s) unless receive_buffer_bytes.nil?
|
|
537
|
+
props.put(kafka::RECONNECT_BACKOFF_MS_CONFIG, reconnect_backoff_ms.to_s) unless reconnect_backoff_ms.nil?
|
|
538
|
+
props.put(kafka::RECONNECT_BACKOFF_MAX_MS_CONFIG, reconnect_backoff_max_ms.to_s) unless reconnect_backoff_max_ms.nil?
|
|
539
|
+
props.put(kafka::REQUEST_TIMEOUT_MS_CONFIG, request_timeout_ms.to_s) unless request_timeout_ms.nil?
|
|
540
|
+
props.put(kafka::RETRY_BACKOFF_MS_CONFIG, retry_backoff_ms.to_s) unless retry_backoff_ms.nil?
|
|
541
|
+
props.put(kafka::SEND_BUFFER_CONFIG, send_buffer_bytes.to_s) unless send_buffer_bytes.nil?
|
|
542
|
+
props.put(kafka::VALUE_DESERIALIZER_CLASS_CONFIG, value_deserializer_class)
|
|
543
|
+
props.put(kafka::ALLOW_AUTO_CREATE_TOPICS_CONFIG, auto_create_topics) unless auto_create_topics.nil?
|
|
544
|
+
props.put(kafka::CHECK_CRCS_CONFIG, check_crcs.to_s) unless check_crcs.nil?
|
|
545
|
+
props.put(kafka::CLIENT_RACK_CONFIG, client_rack) unless client_rack.nil?
|
|
546
|
+
|
|
547
|
+
props.put("security.protocol", security_protocol) unless security_protocol.nil?
|
|
548
|
+
if security_protocol == "SSL"
|
|
549
|
+
set_trustore_keystore_config(props)
|
|
550
|
+
elsif security_protocol == "SASL_PLAINTEXT"
|
|
551
|
+
set_sasl_config(props)
|
|
552
|
+
elsif security_protocol == "SASL_SSL"
|
|
553
|
+
set_trustore_keystore_config(props)
|
|
554
|
+
set_sasl_config(props)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
props
|
|
558
|
+
end
|
|
559
|
+
|
|
443
560
|
def create_consumer(client_id, group_instance_id)
|
|
444
561
|
begin
|
|
445
|
-
props =
|
|
562
|
+
props = build_common_props(client_id)
|
|
446
563
|
kafka = org.apache.kafka.clients.consumer.ConsumerConfig
|
|
447
564
|
|
|
448
565
|
props.put(kafka::AUTO_COMMIT_INTERVAL_MS_CONFIG, auto_commit_interval_ms.to_s) unless auto_commit_interval_ms.nil?
|
|
449
566
|
props.put(kafka::AUTO_OFFSET_RESET_CONFIG, auto_offset_reset) unless auto_offset_reset.nil?
|
|
450
|
-
props.put(kafka::ALLOW_AUTO_CREATE_TOPICS_CONFIG, auto_create_topics) unless auto_create_topics.nil?
|
|
451
|
-
props.put(kafka::BOOTSTRAP_SERVERS_CONFIG, bootstrap_servers)
|
|
452
|
-
props.put(kafka::CHECK_CRCS_CONFIG, check_crcs.to_s) unless check_crcs.nil?
|
|
453
|
-
props.put(kafka::CLIENT_DNS_LOOKUP_CONFIG, client_dns_lookup)
|
|
454
|
-
props.put(kafka::CLIENT_ID_CONFIG, client_id)
|
|
455
|
-
props.put(kafka::CONNECTIONS_MAX_IDLE_MS_CONFIG, connections_max_idle_ms.to_s) unless connections_max_idle_ms.nil?
|
|
456
567
|
props.put(kafka::ENABLE_AUTO_COMMIT_CONFIG, enable_auto_commit.to_s)
|
|
457
568
|
props.put(kafka::EXCLUDE_INTERNAL_TOPICS_CONFIG, exclude_internal_topics) unless exclude_internal_topics.nil?
|
|
458
|
-
props.put(kafka::FETCH_MAX_BYTES_CONFIG, fetch_max_bytes.to_s) unless fetch_max_bytes.nil?
|
|
459
|
-
props.put(kafka::FETCH_MAX_WAIT_MS_CONFIG, fetch_max_wait_ms.to_s) unless fetch_max_wait_ms.nil?
|
|
460
|
-
props.put(kafka::FETCH_MIN_BYTES_CONFIG, fetch_min_bytes.to_s) unless fetch_min_bytes.nil?
|
|
461
|
-
props.put(kafka::GROUP_ID_CONFIG, group_id)
|
|
462
569
|
props.put(kafka::GROUP_INSTANCE_ID_CONFIG, group_instance_id) unless group_instance_id.nil?
|
|
463
570
|
props.put(kafka::GROUP_PROTOCOL_CONFIG, group_protocol)
|
|
464
571
|
props.put(kafka::HEARTBEAT_INTERVAL_MS_CONFIG, heartbeat_interval_ms.to_s) unless heartbeat_interval_ms.nil?
|
|
465
572
|
props.put(kafka::ISOLATION_LEVEL_CONFIG, isolation_level)
|
|
466
|
-
props.put(kafka::KEY_DESERIALIZER_CLASS_CONFIG, key_deserializer_class)
|
|
467
|
-
props.put(kafka::MAX_PARTITION_FETCH_BYTES_CONFIG, max_partition_fetch_bytes.to_s) unless max_partition_fetch_bytes.nil?
|
|
468
|
-
props.put(kafka::MAX_POLL_RECORDS_CONFIG, max_poll_records.to_s) unless max_poll_records.nil?
|
|
469
|
-
props.put(kafka::MAX_POLL_INTERVAL_MS_CONFIG, max_poll_interval_ms.to_s) unless max_poll_interval_ms.nil?
|
|
470
|
-
props.put(kafka::METADATA_MAX_AGE_CONFIG, metadata_max_age_ms.to_s) unless metadata_max_age_ms.nil?
|
|
471
573
|
props.put(kafka::PARTITION_ASSIGNMENT_STRATEGY_CONFIG, partition_assignment_strategy_class) unless partition_assignment_strategy.nil?
|
|
472
|
-
props.put(kafka::RECEIVE_BUFFER_CONFIG, receive_buffer_bytes.to_s) unless receive_buffer_bytes.nil?
|
|
473
|
-
props.put(kafka::RECONNECT_BACKOFF_MS_CONFIG, reconnect_backoff_ms.to_s) unless reconnect_backoff_ms.nil?
|
|
474
|
-
props.put(kafka::RECONNECT_BACKOFF_MAX_MS_CONFIG, reconnect_backoff_max_ms.to_s) unless reconnect_backoff_max_ms.nil?
|
|
475
|
-
props.put(kafka::REQUEST_TIMEOUT_MS_CONFIG, request_timeout_ms.to_s) unless request_timeout_ms.nil?
|
|
476
|
-
props.put(kafka::RETRY_BACKOFF_MS_CONFIG, retry_backoff_ms.to_s) unless retry_backoff_ms.nil?
|
|
477
|
-
props.put(kafka::SEND_BUFFER_CONFIG, send_buffer_bytes.to_s) unless send_buffer_bytes.nil?
|
|
478
574
|
props.put(kafka::SESSION_TIMEOUT_MS_CONFIG, session_timeout_ms.to_s) unless session_timeout_ms.nil?
|
|
479
|
-
props.put(kafka::VALUE_DESERIALIZER_CLASS_CONFIG, value_deserializer_class)
|
|
480
|
-
props.put(kafka::CLIENT_RACK_CONFIG, client_rack) unless client_rack.nil?
|
|
481
575
|
|
|
482
|
-
props.put("security.protocol", security_protocol) unless security_protocol.nil?
|
|
483
576
|
if schema_registry_url
|
|
484
577
|
props.put(kafka::VALUE_DESERIALIZER_CLASS_CONFIG, Java::io.confluent.kafka.serializers.KafkaAvroDeserializer.java_class)
|
|
485
578
|
serdes_config = Java::io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig
|
|
@@ -495,14 +588,6 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
|
|
|
495
588
|
props.put(serdes_config::BASIC_AUTH_CREDENTIALS_SOURCE, 'URL')
|
|
496
589
|
end
|
|
497
590
|
end
|
|
498
|
-
if security_protocol == "SSL"
|
|
499
|
-
set_trustore_keystore_config(props)
|
|
500
|
-
elsif security_protocol == "SASL_PLAINTEXT"
|
|
501
|
-
set_sasl_config(props)
|
|
502
|
-
elsif security_protocol == "SASL_SSL"
|
|
503
|
-
set_trustore_keystore_config(props)
|
|
504
|
-
set_sasl_config(props)
|
|
505
|
-
end
|
|
506
591
|
if schema_registry_ssl_truststore_location
|
|
507
592
|
props.put('schema.registry.ssl.truststore.location', schema_registry_ssl_truststore_location)
|
|
508
593
|
props.put('schema.registry.ssl.truststore.password', schema_registry_ssl_truststore_password.value)
|
|
@@ -565,4 +650,100 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
|
|
|
565
650
|
!header.nil? && !header.value.nil? && !header.key.nil?
|
|
566
651
|
end
|
|
567
652
|
|
|
653
|
+
def validate_share_group_config!
|
|
654
|
+
incompatible = original_params.keys & SHARE_GROUP_INCOMPATIBLE_OPTIONS
|
|
655
|
+
unless incompatible.empty?
|
|
656
|
+
raise LogStash::ConfigurationError,
|
|
657
|
+
"The following options are not compatible with share_group consumer_mode and must be removed: #{incompatible.join(', ')}"
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def validate_consumer_group_config!
|
|
662
|
+
incompatible = original_params.keys & CONSUMER_GROUP_ONLY_OPTIONS
|
|
663
|
+
unless incompatible.empty?
|
|
664
|
+
raise LogStash::ConfigurationError,
|
|
665
|
+
"The following options require consumer_mode => 'share_group' and cannot be used with consumer_group mode: #{incompatible.join(', ')}"
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def create_share_consumer(client_id)
|
|
670
|
+
begin
|
|
671
|
+
props = build_common_props(client_id)
|
|
672
|
+
|
|
673
|
+
# Share consumer defaults to implicit acknowledgement, which disallows explicit
|
|
674
|
+
# acknowledge() calls. The plugin acknowledges each record after processing, so
|
|
675
|
+
# explicit mode is required.
|
|
676
|
+
props.put("share.acknowledgement.mode", "explicit")
|
|
677
|
+
|
|
678
|
+
org.apache.kafka.clients.consumer.KafkaShareConsumer.new(props)
|
|
679
|
+
rescue => e
|
|
680
|
+
logger.error("Unable to create Kafka share consumer from given configuration",
|
|
681
|
+
:kafka_error_message => e,
|
|
682
|
+
:cause => e.respond_to?(:getCause) ? e.getCause() : nil)
|
|
683
|
+
raise e
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def share_group_thread_runner(logstash_queue, consumer, name)
|
|
688
|
+
java.lang.Thread.new do
|
|
689
|
+
LogStash::Util::set_thread_name(name)
|
|
690
|
+
begin
|
|
691
|
+
codec_instance = @codec.clone
|
|
692
|
+
ack_accept = org.apache.kafka.clients.consumer.AcknowledgeType::ACCEPT
|
|
693
|
+
ack_release = org.apache.kafka.clients.consumer.AcknowledgeType::RELEASE
|
|
694
|
+
until stop?
|
|
695
|
+
records = do_poll(consumer)
|
|
696
|
+
unless records.empty?
|
|
697
|
+
records.each do |record|
|
|
698
|
+
if stop?
|
|
699
|
+
consumer.acknowledge(record, ack_release)
|
|
700
|
+
else
|
|
701
|
+
begin
|
|
702
|
+
handle_record(record, codec_instance, logstash_queue)
|
|
703
|
+
consumer.acknowledge(record, ack_accept)
|
|
704
|
+
rescue => e
|
|
705
|
+
logger.error("Error processing record in share group mode, acknowledging with #{@share_group_acknowledgement_on_error}",
|
|
706
|
+
:kafka_error_message => e,
|
|
707
|
+
:cause => e.respond_to?(:getCause) ? e.getCause() : nil)
|
|
708
|
+
consumer.acknowledge(record, @share_group_ack_error_type)
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
commit_share_acknowledgements(consumer)
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
ensure
|
|
716
|
+
consumer.close
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def commit_share_acknowledgements(consumer)
|
|
722
|
+
begin
|
|
723
|
+
log_rejected_acknowledgements(consumer.commitSync)
|
|
724
|
+
rescue org.apache.kafka.common.errors.WakeupException => e
|
|
725
|
+
logger.debug("Wake up from share consumer commitSync", :kafka_error_message => e)
|
|
726
|
+
raise e unless stop?
|
|
727
|
+
rescue StandardError => e
|
|
728
|
+
logger.error("Unable to commit share group acknowledgements",
|
|
729
|
+
:kafka_error_message => e,
|
|
730
|
+
:cause => e.respond_to?(:getCause) ? e.getCause() : nil)
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Unlike `KafkaConsumer#commitSync`, which returns void and raises on failure,
|
|
735
|
+
# `ShareConsumer#commitSync` reports per-partition acknowledgement failures through its
|
|
736
|
+
# return value and only raises for unrecoverable errors.
|
|
737
|
+
def log_rejected_acknowledgements(results)
|
|
738
|
+
return if results.nil?
|
|
739
|
+
|
|
740
|
+
results.each do |topic_id_partition, maybe_error|
|
|
741
|
+
next unless maybe_error && maybe_error.isPresent
|
|
742
|
+
logger.warn("Share group acknowledgements were rejected, the affected records will be redelivered",
|
|
743
|
+
:topic => topic_id_partition.topic,
|
|
744
|
+
:partition => topic_id_partition.partition,
|
|
745
|
+
:kafka_error_message => maybe_error.get)
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
|
|
568
749
|
end #class LogStash::Inputs::Kafka
|
|
@@ -14,7 +14,7 @@ require_jar('com.fasterxml.jackson.core', 'jackson-core', '2.21.7')
|
|
|
14
14
|
require_jar('org.apache.kafka', 'kafka-clients', '4.2.0')
|
|
15
15
|
require_jar('org.apache.httpcomponents.client5', 'httpclient5', '5.6.5')
|
|
16
16
|
require_jar('com.github.luben', 'zstd-jni', '1.5.7-24')
|
|
17
|
-
require_jar('at.yawk.lz4', 'lz4-java', '1.12.
|
|
17
|
+
require_jar('at.yawk.lz4', 'lz4-java', '1.12.1')
|
|
18
18
|
require_jar('org.xerial.snappy', 'snappy-java', '1.1.10.12')
|
|
19
19
|
require_jar('com.google.guava', 'guava', '33.7.2-jre')
|
|
20
20
|
require_jar('com.fasterxml.jackson.core', 'jackson-annotations', '2.21')
|
|
@@ -130,6 +130,97 @@ describe "inputs/kafka", :integration => true do
|
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
context "#share-group" do
|
|
134
|
+
# Self-contained: the topic and the group's share.auto.offset.reset are set up here rather
|
|
135
|
+
# than in kafka_test_setup.sh, so these specs run against any broker with share groups
|
|
136
|
+
# available. Both names are unique per example, which keeps share-group state (which is
|
|
137
|
+
# persistent per group/topic) from leaking between examples or between runs.
|
|
138
|
+
let(:run_id) { rand(36**8).to_s(36) }
|
|
139
|
+
let(:share_group_topic) { "logstash_integration_share_group_topic_#{run_id}" }
|
|
140
|
+
let(:share_group_id) { "logstash_integration_share_group_#{run_id}" }
|
|
141
|
+
let(:share_group_partitions) { 3 }
|
|
142
|
+
let(:share_group_msg_count) { 30 }
|
|
143
|
+
let(:share_group_config) do
|
|
144
|
+
{ 'topics' => [share_group_topic], 'group_id' => share_group_id,
|
|
145
|
+
'consumer_mode' => 'share_group', 'consumer_threads' => 3,
|
|
146
|
+
'codec' => 'plain', 'bootstrap_servers' => 'localhost:9092' }
|
|
147
|
+
end
|
|
148
|
+
let(:produced_messages) { share_group_msg_count.times.map { |i| "share-msg-#{run_id}-#{i}" } }
|
|
149
|
+
|
|
150
|
+
def with_admin_client
|
|
151
|
+
props = java.util.Properties.new
|
|
152
|
+
props.put(org.apache.kafka.clients.admin.AdminClientConfig::BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
|
|
153
|
+
admin = org.apache.kafka.clients.admin.AdminClient.create(props)
|
|
154
|
+
begin
|
|
155
|
+
yield admin
|
|
156
|
+
ensure
|
|
157
|
+
admin.close
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Share groups default to share.auto.offset.reset=latest. Pinning it to earliest lets the
|
|
162
|
+
# specs produce up front instead of racing the group to a stable epoch before producing.
|
|
163
|
+
def set_share_group_offset_reset(admin, group_id, value)
|
|
164
|
+
resource = org.apache.kafka.common.config.ConfigResource.new(
|
|
165
|
+
org.apache.kafka.common.config.ConfigResource::Type::GROUP, group_id)
|
|
166
|
+
ops = java.util.ArrayList.new
|
|
167
|
+
ops.add(org.apache.kafka.clients.admin.AlterConfigOp.new(
|
|
168
|
+
org.apache.kafka.clients.admin.ConfigEntry.new('share.auto.offset.reset', value),
|
|
169
|
+
org.apache.kafka.clients.admin.AlterConfigOp::OpType::SET))
|
|
170
|
+
changes = java.util.HashMap.new
|
|
171
|
+
changes.put(resource, ops)
|
|
172
|
+
admin.incrementalAlterConfigs(changes).all.get
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
before do
|
|
176
|
+
with_admin_client do |admin|
|
|
177
|
+
admin.createTopics([org.apache.kafka.clients.admin.NewTopic.new(
|
|
178
|
+
share_group_topic, share_group_partitions, 1.to_java(:short))]).all.get
|
|
179
|
+
set_share_group_offset_reset(admin, share_group_id, 'earliest')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# One producer for the whole batch; send_message opens and closes one per record.
|
|
183
|
+
props = java.util.Properties.new
|
|
184
|
+
kafka = org.apache.kafka.clients.producer.ProducerConfig
|
|
185
|
+
props.put(kafka::BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
|
|
186
|
+
props.put(kafka::KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer")
|
|
187
|
+
props.put(kafka::VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer")
|
|
188
|
+
producer = org.apache.kafka.clients.producer.KafkaProducer.new(props)
|
|
189
|
+
begin
|
|
190
|
+
produced_messages.each do |payload|
|
|
191
|
+
producer.send(org.apache.kafka.clients.producer.ProducerRecord.new(share_group_topic, payload))
|
|
192
|
+
end
|
|
193
|
+
producer.flush
|
|
194
|
+
ensure
|
|
195
|
+
producer.close
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
after do
|
|
200
|
+
with_admin_client { |admin| admin.deleteTopics([share_group_topic]).all.get } rescue nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def collect_share_group_messages(config)
|
|
204
|
+
consume_messages(config, timeout: false, event_count: share_group_msg_count) do |queue, _input|
|
|
205
|
+
# Not `eq`: share groups are at-least-once, so assert on the distinct payloads
|
|
206
|
+
# rather than failing the wait outright if a record gets redelivered.
|
|
207
|
+
wait(timeout_seconds).for { queue.length }.to be >= share_group_msg_count
|
|
208
|
+
queue.length.times.map { queue.pop.get("message") }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "delivers every produced record across the consumer threads" do
|
|
213
|
+
expect(collect_share_group_messages(share_group_config).uniq).to match_array(produced_messages)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "is not bounded by the partition count" do
|
|
217
|
+
# More threads than partitions. In consumer_group mode the surplus threads would
|
|
218
|
+
# necessarily sit idle; a share group has no exclusive partition assignment.
|
|
219
|
+
config = share_group_config.merge('consumer_threads' => share_group_partitions + 2)
|
|
220
|
+
expect(collect_share_group_messages(config).uniq).to match_array(produced_messages)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
133
224
|
context "#kafka-decorate" do
|
|
134
225
|
it "should show the right topic and group name in decorated kafka section" do
|
|
135
226
|
start = LogStash::Timestamp.now.time.to_i
|
|
@@ -84,6 +84,11 @@ describe LogStash::Inputs::Kafka do
|
|
|
84
84
|
expect { subject.register }.to_not raise_error
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
it "raises ConfigurationError when share_group_acknowledgement_on_error is set in consumer_group mode" do
|
|
88
|
+
config['share_group_acknowledgement_on_error'] = 'reject'
|
|
89
|
+
expect { subject.register }.to raise_error(LogStash::ConfigurationError, /share_group_acknowledgement_on_error/)
|
|
90
|
+
end
|
|
91
|
+
|
|
87
92
|
context "when the deprecated `default` is specified" do
|
|
88
93
|
let(:config) { common_config.merge('client_dns_lookup' => 'default') }
|
|
89
94
|
|
|
@@ -495,4 +500,219 @@ describe LogStash::Inputs::Kafka do
|
|
|
495
500
|
expect( subject.enable_auto_commit ).to be true
|
|
496
501
|
end
|
|
497
502
|
end
|
|
503
|
+
|
|
504
|
+
context 'share group mode' do
|
|
505
|
+
let(:share_consumer_double) { double('kafka-share-consumer') }
|
|
506
|
+
let(:config) { common_config.merge('consumer_mode' => 'share_group') }
|
|
507
|
+
|
|
508
|
+
before do
|
|
509
|
+
allow(share_consumer_double).to receive(:subscribe)
|
|
510
|
+
allow(share_consumer_double).to receive(:wakeup)
|
|
511
|
+
allow(share_consumer_double).to receive(:close)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
describe '#register' do
|
|
515
|
+
it "registers without error" do
|
|
516
|
+
expect { subject.register }.not_to raise_error
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
it "raises ConfigurationError when group_instance_id is set" do
|
|
520
|
+
config['group_instance_id'] = 'my-instance'
|
|
521
|
+
expect { subject.register }.to raise_error(LogStash::ConfigurationError, /group_instance_id/)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
it "raises ConfigurationError when partition_assignment_strategy is set" do
|
|
525
|
+
config['partition_assignment_strategy'] = 'sticky'
|
|
526
|
+
expect { subject.register }.to raise_error(LogStash::ConfigurationError, /partition_assignment_strategy/)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
it "raises ConfigurationError when schema_registry_url is set" do
|
|
530
|
+
config['schema_registry_url'] = 'http://localhost:8081'
|
|
531
|
+
expect { subject.register }.to raise_error(LogStash::ConfigurationError, /schema_registry_url/)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
%w[
|
|
535
|
+
enable_auto_commit
|
|
536
|
+
auto_commit_interval_ms
|
|
537
|
+
heartbeat_interval_ms
|
|
538
|
+
session_timeout_ms
|
|
539
|
+
isolation_level
|
|
540
|
+
auto_offset_reset
|
|
541
|
+
exclude_internal_topics
|
|
542
|
+
group_protocol
|
|
543
|
+
].each do |opt|
|
|
544
|
+
it "raises ConfigurationError when #{opt} is set in the pipeline config" do
|
|
545
|
+
config[opt] = case opt
|
|
546
|
+
when 'enable_auto_commit', 'exclude_internal_topics' then 'true'
|
|
547
|
+
when 'auto_commit_interval_ms', 'heartbeat_interval_ms', 'session_timeout_ms' then '5000'
|
|
548
|
+
when 'isolation_level' then 'read_committed'
|
|
549
|
+
when 'auto_offset_reset' then 'earliest'
|
|
550
|
+
when 'group_protocol' then 'consumer'
|
|
551
|
+
end
|
|
552
|
+
expect { subject.register }.to raise_error(LogStash::ConfigurationError, /#{opt}/)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
it "accepts check_crcs in share_group mode (supported by KafkaShareConsumer)" do
|
|
557
|
+
config['check_crcs'] = true
|
|
558
|
+
expect { subject.register }.not_to raise_error
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
it "accepts auto_create_topics in share_group mode (supported by KafkaShareConsumer)" do
|
|
562
|
+
config['auto_create_topics'] = true
|
|
563
|
+
expect { subject.register }.not_to raise_error
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
describe '#create_share_consumer' do
|
|
568
|
+
it "creates a KafkaShareConsumer with correct bootstrap_servers and group_id" do
|
|
569
|
+
expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
|
|
570
|
+
to receive(:new).with(hash_including(
|
|
571
|
+
'bootstrap.servers' => 'localhost:9092',
|
|
572
|
+
'group.id' => 'logstash'
|
|
573
|
+
)).and_return(share_consumer_double)
|
|
574
|
+
|
|
575
|
+
expect(subject.send(:create_share_consumer, 'test-share-client-0')).to be share_consumer_double
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
it "does not set enable.auto.commit, isolation.level or auto.offset.reset" do
|
|
579
|
+
expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
|
|
580
|
+
to receive(:new) do |props|
|
|
581
|
+
expect(props).not_to have_key('enable.auto.commit')
|
|
582
|
+
expect(props).not_to have_key('isolation.level')
|
|
583
|
+
expect(props).not_to have_key('auto.offset.reset')
|
|
584
|
+
share_consumer_double
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
subject.send(:create_share_consumer, 'test-share-client-0')
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
it "sets share.acknowledgement.mode to explicit" do
|
|
591
|
+
expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
|
|
592
|
+
to receive(:new).with(hash_including('share.acknowledgement.mode' => 'explicit')).
|
|
593
|
+
and_return(share_consumer_double)
|
|
594
|
+
|
|
595
|
+
subject.send(:create_share_consumer, 'test-share-client-0')
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
context 'with client_rack' do
|
|
599
|
+
let(:config) { super().merge('client_rack' => 'US-EAST-1') }
|
|
600
|
+
|
|
601
|
+
it "passes client.rack to the share consumer" do
|
|
602
|
+
expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
|
|
603
|
+
to receive(:new).with(hash_including('client.rack' => 'US-EAST-1')).
|
|
604
|
+
and_return(share_consumer_double)
|
|
605
|
+
|
|
606
|
+
subject.send(:create_share_consumer, 'test-share-client-0')
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
describe '#running in share_group mode' do
|
|
612
|
+
let(:q) { Queue.new }
|
|
613
|
+
let(:payload) do
|
|
614
|
+
5.times.map { org.apache.kafka.clients.consumer.ConsumerRecord.new("logstash", 0, 0, "key", "value") }
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
before do
|
|
618
|
+
subject.register
|
|
619
|
+
expect(subject).to receive(:create_share_consumer).once.and_return(share_consumer_double)
|
|
620
|
+
polled = false
|
|
621
|
+
allow(share_consumer_double).to receive(:poll) do
|
|
622
|
+
if polled
|
|
623
|
+
[]
|
|
624
|
+
else
|
|
625
|
+
polled = true
|
|
626
|
+
payload
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
allow(share_consumer_double).to receive(:acknowledge)
|
|
630
|
+
allow(share_consumer_double).to receive(:commitSync)
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
it "processes events and acknowledges each record" do
|
|
634
|
+
expect(share_consumer_double).to receive(:acknowledge).exactly(5).times
|
|
635
|
+
expect(share_consumer_double).to receive(:commitSync).at_least(:once)
|
|
636
|
+
|
|
637
|
+
t = Thread.new { sleep(1); subject.do_stop }
|
|
638
|
+
subject.run(q)
|
|
639
|
+
t.join
|
|
640
|
+
|
|
641
|
+
expect(q.size).to eq(5)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
it "acknowledges with ACCEPT on successful record processing" do
|
|
645
|
+
ack_accept = org.apache.kafka.clients.consumer.AcknowledgeType::ACCEPT
|
|
646
|
+
expect(share_consumer_double).to receive(:acknowledge).with(anything, ack_accept).exactly(5).times
|
|
647
|
+
allow(share_consumer_double).to receive(:commitSync)
|
|
648
|
+
|
|
649
|
+
t = Thread.new { sleep(1); subject.do_stop }
|
|
650
|
+
subject.run(q)
|
|
651
|
+
t.join
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
describe '#commit_share_acknowledgements' do
|
|
656
|
+
# `ShareConsumer#commitSync` reports per-partition acknowledgement failures in its
|
|
657
|
+
# return value instead of raising, so they are only visible if the plugin inspects it.
|
|
658
|
+
let(:topic_id_partition) do
|
|
659
|
+
org.apache.kafka.common.TopicIdPartition.new(
|
|
660
|
+
org.apache.kafka.common.Uuid.randomUuid, 3, "logstash")
|
|
661
|
+
end
|
|
662
|
+
let(:rejected) do
|
|
663
|
+
java.util.Optional.of(
|
|
664
|
+
org.apache.kafka.common.errors.InvalidRecordStateException.new("acquisition lock expired"))
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
before { subject.register }
|
|
668
|
+
|
|
669
|
+
it "warns about acknowledgements the broker rejected" do
|
|
670
|
+
allow(share_consumer_double).to receive(:commitSync).and_return({ topic_id_partition => rejected })
|
|
671
|
+
|
|
672
|
+
expect(subject.logger).to receive(:warn).with(
|
|
673
|
+
/rejected/, hash_including(:topic => "logstash", :partition => 3))
|
|
674
|
+
|
|
675
|
+
subject.send(:commit_share_acknowledgements, share_consumer_double)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
it "stays quiet when every acknowledgement succeeded" do
|
|
679
|
+
allow(share_consumer_double).to receive(:commitSync).
|
|
680
|
+
and_return({ topic_id_partition => java.util.Optional.empty })
|
|
681
|
+
|
|
682
|
+
expect(subject.logger).to_not receive(:warn)
|
|
683
|
+
|
|
684
|
+
subject.send(:commit_share_acknowledgements, share_consumer_double)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
it "tolerates the empty map returned when there was nothing to acknowledge" do
|
|
688
|
+
allow(share_consumer_double).to receive(:commitSync).and_return(java.util.Collections.emptyMap)
|
|
689
|
+
|
|
690
|
+
expect(subject.logger).to_not receive(:warn)
|
|
691
|
+
|
|
692
|
+
expect { subject.send(:commit_share_acknowledgements, share_consumer_double) }.to_not raise_error
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
describe 'acknowledgement_on_error' do
|
|
697
|
+
before { subject.register }
|
|
698
|
+
|
|
699
|
+
context 'with release strategy (default)' do
|
|
700
|
+
it "uses RELEASE ack type on error" do
|
|
701
|
+
expect(subject.instance_variable_get(:@share_group_ack_error_type)).to eq(
|
|
702
|
+
org.apache.kafka.clients.consumer.AcknowledgeType::RELEASE
|
|
703
|
+
)
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
context 'with reject strategy' do
|
|
708
|
+
let(:config) { super().merge('share_group_acknowledgement_on_error' => 'reject') }
|
|
709
|
+
|
|
710
|
+
it "uses REJECT ack type on error" do
|
|
711
|
+
expect(subject.instance_variable_get(:@share_group_ack_error_type)).to eq(
|
|
712
|
+
org.apache.kafka.clients.consumer.AcknowledgeType::REJECT
|
|
713
|
+
)
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
end
|
|
498
718
|
end
|
|
Binary file
|
data/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
12.
|
|
1
|
+
12.2.0
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: logstash-integration-kafka
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 12.
|
|
4
|
+
version: 12.2.0
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Elastic
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: logstash-core-plugin-api
|
|
@@ -244,7 +244,7 @@ files:
|
|
|
244
244
|
- spec/unit/inputs/avro_schema_fixture_payment.asvc
|
|
245
245
|
- spec/unit/inputs/kafka_spec.rb
|
|
246
246
|
- spec/unit/outputs/kafka_spec.rb
|
|
247
|
-
- vendor/jar-dependencies/at/yawk/lz4/lz4-java/1.12.
|
|
247
|
+
- vendor/jar-dependencies/at/yawk/lz4/lz4-java/1.12.1/lz4-java-1.12.1.jar
|
|
248
248
|
- vendor/jar-dependencies/com/eclipsesource/minimal-json/minimal-json/0.9.5/minimal-json-0.9.5.jar
|
|
249
249
|
- vendor/jar-dependencies/com/fasterxml/jackson/core/jackson-annotations/2.21/jackson-annotations-2.21.jar
|
|
250
250
|
- vendor/jar-dependencies/com/fasterxml/jackson/core/jackson-core/2.21.7/jackson-core-2.21.7.jar
|
|
@@ -303,7 +303,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
303
303
|
- !ruby/object:Gem::Version
|
|
304
304
|
version: '0'
|
|
305
305
|
requirements: []
|
|
306
|
-
rubygems_version: 3.
|
|
306
|
+
rubygems_version: 3.7.2
|
|
307
307
|
specification_version: 4
|
|
308
308
|
summary: Integration with Kafka - input and output plugins
|
|
309
309
|
test_files:
|