karafka-rdkafka 0.21.0-aarch64-linux-gnu

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.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +3 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/workflows/ci_linux_alpine_x86_64_musl.yml +197 -0
  5. data/.github/workflows/ci_linux_alpine_x86_64_musl_complementary.yml +264 -0
  6. data/.github/workflows/ci_linux_debian_x86_64_gnu.yml +271 -0
  7. data/.github/workflows/ci_linux_debian_x86_64_gnu_complementary.yml +334 -0
  8. data/.github/workflows/ci_linux_ubuntu_aarch64_gnu.yml +271 -0
  9. data/.github/workflows/ci_linux_ubuntu_aarch64_gnu_complementary.yml +295 -0
  10. data/.github/workflows/ci_linux_ubuntu_x86_64_gnu.yml +281 -0
  11. data/.github/workflows/ci_linux_ubuntu_x86_64_gnu_complementary.yml +294 -0
  12. data/.github/workflows/ci_macos_arm64.yml +284 -0
  13. data/.github/workflows/push_linux_aarch64_gnu.yml +65 -0
  14. data/.github/workflows/push_linux_x86_64_gnu.yml +65 -0
  15. data/.github/workflows/push_linux_x86_64_musl.yml +79 -0
  16. data/.github/workflows/push_macos_arm64.yml +54 -0
  17. data/.github/workflows/push_ruby.yml +37 -0
  18. data/.github/workflows/trigger-wiki-refresh.yml +30 -0
  19. data/.github/workflows/verify-action-pins.yml +16 -0
  20. data/.gitignore +16 -0
  21. data/.rspec +3 -0
  22. data/.ruby-gemset +1 -0
  23. data/.ruby-version +1 -0
  24. data/.yardopts +2 -0
  25. data/CHANGELOG.md +344 -0
  26. data/Gemfile +5 -0
  27. data/MIT-LICENSE +22 -0
  28. data/README.md +78 -0
  29. data/Rakefile +96 -0
  30. data/dist/cyrus-sasl-2.1.28.tar.gz +0 -0
  31. data/dist/krb5-1.21.3.tar.gz +0 -0
  32. data/dist/openssl-3.0.16.tar.gz +0 -0
  33. data/dist/zlib-1.3.1.tar.gz +0 -0
  34. data/dist/zstd-1.5.7.tar.gz +0 -0
  35. data/docker-compose-ssl.yml +35 -0
  36. data/docker-compose.yml +25 -0
  37. data/ext/README.md +19 -0
  38. data/ext/Rakefile +131 -0
  39. data/ext/build_common.sh +376 -0
  40. data/ext/build_linux_aarch64_gnu.sh +326 -0
  41. data/ext/build_linux_x86_64_gnu.sh +317 -0
  42. data/ext/build_linux_x86_64_musl.sh +773 -0
  43. data/ext/build_macos_arm64.sh +557 -0
  44. data/ext/generate-ssl-certs.sh +109 -0
  45. data/ext/librdkafka.so +0 -0
  46. data/karafka-rdkafka.gemspec +65 -0
  47. data/lib/rdkafka/abstract_handle.rb +116 -0
  48. data/lib/rdkafka/admin/acl_binding_result.rb +51 -0
  49. data/lib/rdkafka/admin/config_binding_result.rb +30 -0
  50. data/lib/rdkafka/admin/config_resource_binding_result.rb +18 -0
  51. data/lib/rdkafka/admin/create_acl_handle.rb +28 -0
  52. data/lib/rdkafka/admin/create_acl_report.rb +24 -0
  53. data/lib/rdkafka/admin/create_partitions_handle.rb +30 -0
  54. data/lib/rdkafka/admin/create_partitions_report.rb +6 -0
  55. data/lib/rdkafka/admin/create_topic_handle.rb +32 -0
  56. data/lib/rdkafka/admin/create_topic_report.rb +24 -0
  57. data/lib/rdkafka/admin/delete_acl_handle.rb +30 -0
  58. data/lib/rdkafka/admin/delete_acl_report.rb +23 -0
  59. data/lib/rdkafka/admin/delete_groups_handle.rb +28 -0
  60. data/lib/rdkafka/admin/delete_groups_report.rb +24 -0
  61. data/lib/rdkafka/admin/delete_topic_handle.rb +32 -0
  62. data/lib/rdkafka/admin/delete_topic_report.rb +24 -0
  63. data/lib/rdkafka/admin/describe_acl_handle.rb +30 -0
  64. data/lib/rdkafka/admin/describe_acl_report.rb +24 -0
  65. data/lib/rdkafka/admin/describe_configs_handle.rb +33 -0
  66. data/lib/rdkafka/admin/describe_configs_report.rb +48 -0
  67. data/lib/rdkafka/admin/incremental_alter_configs_handle.rb +33 -0
  68. data/lib/rdkafka/admin/incremental_alter_configs_report.rb +48 -0
  69. data/lib/rdkafka/admin.rb +832 -0
  70. data/lib/rdkafka/bindings.rb +583 -0
  71. data/lib/rdkafka/callbacks.rb +415 -0
  72. data/lib/rdkafka/config.rb +395 -0
  73. data/lib/rdkafka/consumer/headers.rb +79 -0
  74. data/lib/rdkafka/consumer/message.rb +86 -0
  75. data/lib/rdkafka/consumer/partition.rb +57 -0
  76. data/lib/rdkafka/consumer/topic_partition_list.rb +190 -0
  77. data/lib/rdkafka/consumer.rb +663 -0
  78. data/lib/rdkafka/error.rb +201 -0
  79. data/lib/rdkafka/helpers/oauth.rb +58 -0
  80. data/lib/rdkafka/helpers/time.rb +14 -0
  81. data/lib/rdkafka/metadata.rb +115 -0
  82. data/lib/rdkafka/native_kafka.rb +139 -0
  83. data/lib/rdkafka/producer/delivery_handle.rb +48 -0
  84. data/lib/rdkafka/producer/delivery_report.rb +45 -0
  85. data/lib/rdkafka/producer/partitions_count_cache.rb +216 -0
  86. data/lib/rdkafka/producer.rb +497 -0
  87. data/lib/rdkafka/version.rb +7 -0
  88. data/lib/rdkafka.rb +54 -0
  89. data/renovate.json +92 -0
  90. data/spec/integrations/ssl_stress_spec.rb +121 -0
  91. data/spec/lib/rdkafka/abstract_handle_spec.rb +117 -0
  92. data/spec/lib/rdkafka/admin/create_acl_handle_spec.rb +56 -0
  93. data/spec/lib/rdkafka/admin/create_acl_report_spec.rb +18 -0
  94. data/spec/lib/rdkafka/admin/create_topic_handle_spec.rb +54 -0
  95. data/spec/lib/rdkafka/admin/create_topic_report_spec.rb +16 -0
  96. data/spec/lib/rdkafka/admin/delete_acl_handle_spec.rb +85 -0
  97. data/spec/lib/rdkafka/admin/delete_acl_report_spec.rb +72 -0
  98. data/spec/lib/rdkafka/admin/delete_topic_handle_spec.rb +54 -0
  99. data/spec/lib/rdkafka/admin/delete_topic_report_spec.rb +16 -0
  100. data/spec/lib/rdkafka/admin/describe_acl_handle_spec.rb +85 -0
  101. data/spec/lib/rdkafka/admin/describe_acl_report_spec.rb +73 -0
  102. data/spec/lib/rdkafka/admin_spec.rb +982 -0
  103. data/spec/lib/rdkafka/bindings_spec.rb +198 -0
  104. data/spec/lib/rdkafka/callbacks_spec.rb +20 -0
  105. data/spec/lib/rdkafka/config_spec.rb +258 -0
  106. data/spec/lib/rdkafka/consumer/headers_spec.rb +73 -0
  107. data/spec/lib/rdkafka/consumer/message_spec.rb +139 -0
  108. data/spec/lib/rdkafka/consumer/partition_spec.rb +57 -0
  109. data/spec/lib/rdkafka/consumer/topic_partition_list_spec.rb +248 -0
  110. data/spec/lib/rdkafka/consumer_spec.rb +1343 -0
  111. data/spec/lib/rdkafka/error_spec.rb +95 -0
  112. data/spec/lib/rdkafka/metadata_spec.rb +79 -0
  113. data/spec/lib/rdkafka/native_kafka_spec.rb +130 -0
  114. data/spec/lib/rdkafka/producer/delivery_handle_spec.rb +60 -0
  115. data/spec/lib/rdkafka/producer/delivery_report_spec.rb +25 -0
  116. data/spec/lib/rdkafka/producer/partitions_count_cache_spec.rb +359 -0
  117. data/spec/lib/rdkafka/producer_spec.rb +1527 -0
  118. data/spec/spec_helper.rb +230 -0
  119. metadata +320 -0
@@ -0,0 +1,1343 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+ require 'securerandom'
5
+
6
+ describe Rdkafka::Consumer do
7
+ let(:consumer) { rdkafka_consumer_config.consumer }
8
+ let(:producer) { rdkafka_producer_config.producer }
9
+
10
+ after { consumer.close }
11
+ after { producer.close }
12
+
13
+ describe '#name' do
14
+ it { expect(consumer.name).to include('rdkafka#consumer-') }
15
+ end
16
+
17
+ describe 'consumer without auto-start' do
18
+ let(:consumer) { rdkafka_consumer_config.consumer(native_kafka_auto_start: false) }
19
+
20
+ it 'expect to be able to start it later and close' do
21
+ consumer.start
22
+ consumer.close
23
+ end
24
+
25
+ it 'expect to be able to close it without starting' do
26
+ consumer.close
27
+ end
28
+ end
29
+
30
+ describe "#subscribe, #unsubscribe and #subscription" do
31
+ it "should subscribe, unsubscribe and return the subscription" do
32
+ expect(consumer.subscription).to be_empty
33
+
34
+ consumer.subscribe("consume_test_topic")
35
+
36
+ expect(consumer.subscription).not_to be_empty
37
+ expected_subscription = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
38
+ list.add_topic("consume_test_topic")
39
+ end
40
+ expect(consumer.subscription).to eq expected_subscription
41
+
42
+ consumer.unsubscribe
43
+
44
+ expect(consumer.subscription).to be_empty
45
+ end
46
+
47
+ it "should raise an error when subscribing fails" do
48
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_subscribe).and_return(20)
49
+
50
+ expect {
51
+ consumer.subscribe("consume_test_topic")
52
+ }.to raise_error(Rdkafka::RdkafkaError)
53
+ end
54
+
55
+ it "should raise an error when unsubscribing fails" do
56
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_unsubscribe).and_return(20)
57
+
58
+ expect {
59
+ consumer.unsubscribe
60
+ }.to raise_error(Rdkafka::RdkafkaError)
61
+ end
62
+
63
+ it "should raise an error when fetching the subscription fails" do
64
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_subscription).and_return(20)
65
+
66
+ expect {
67
+ consumer.subscription
68
+ }.to raise_error(Rdkafka::RdkafkaError)
69
+ end
70
+
71
+ context "when using consumer without the poll set" do
72
+ let(:consumer) do
73
+ config = rdkafka_consumer_config
74
+ config.consumer_poll_set = false
75
+ config.consumer
76
+ end
77
+
78
+ it "should subscribe, unsubscribe and return the subscription" do
79
+ expect(consumer.subscription).to be_empty
80
+
81
+ consumer.subscribe("consume_test_topic")
82
+
83
+ expect(consumer.subscription).not_to be_empty
84
+ expected_subscription = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
85
+ list.add_topic("consume_test_topic")
86
+ end
87
+ expect(consumer.subscription).to eq expected_subscription
88
+
89
+ consumer.unsubscribe
90
+
91
+ expect(consumer.subscription).to be_empty
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "#pause and #resume" do
97
+ context "subscription" do
98
+ let(:timeout) { 2000 }
99
+
100
+ before { consumer.subscribe("consume_test_topic") }
101
+ after { consumer.unsubscribe }
102
+
103
+ it "should pause and then resume" do
104
+ # 1. partitions are assigned
105
+ wait_for_assignment(consumer)
106
+ expect(consumer.assignment).not_to be_empty
107
+
108
+ # 2. send a first message
109
+ send_one_message
110
+
111
+ # 3. ensure that message is successfully consumed
112
+ records = consumer.poll(timeout)
113
+ expect(records).not_to be_nil
114
+ consumer.commit
115
+
116
+ # 4. send a second message
117
+ send_one_message
118
+
119
+ # 5. pause the subscription
120
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
121
+ tpl.add_topic("consume_test_topic", (0..2))
122
+ consumer.pause(tpl)
123
+
124
+ # 6. ensure that messages are not available
125
+ records = consumer.poll(timeout)
126
+ expect(records).to be_nil
127
+
128
+ # 7. resume the subscription
129
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
130
+ tpl.add_topic("consume_test_topic", (0..2))
131
+ consumer.resume(tpl)
132
+
133
+ # 8. ensure that message is successfully consumed
134
+ records = consumer.poll(timeout)
135
+ expect(records).not_to be_nil
136
+ end
137
+ end
138
+
139
+ it "should raise when not TopicPartitionList" do
140
+ expect { consumer.pause(true) }.to raise_error(TypeError)
141
+ expect { consumer.resume(true) }.to raise_error(TypeError)
142
+ end
143
+
144
+ it "should raise an error when pausing fails" do
145
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap { |tpl| tpl.add_topic('topic', (0..1)) }
146
+
147
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_pause_partitions).and_return(20)
148
+ expect {
149
+ consumer.pause(list)
150
+ }.to raise_error do |err|
151
+ expect(err).to be_instance_of(Rdkafka::RdkafkaTopicPartitionListError)
152
+ expect(err.topic_partition_list).to be
153
+ end
154
+ end
155
+
156
+ it "should raise an error when resume fails" do
157
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_resume_partitions).and_return(20)
158
+ expect {
159
+ consumer.resume(Rdkafka::Consumer::TopicPartitionList.new)
160
+ }.to raise_error Rdkafka::RdkafkaError
161
+ end
162
+
163
+ def send_one_message
164
+ producer.produce(
165
+ topic: "consume_test_topic",
166
+ payload: "payload 1",
167
+ key: "key 1"
168
+ ).wait
169
+ end
170
+ end
171
+
172
+ describe "#seek" do
173
+ let(:topic) { "it-#{SecureRandom.uuid}" }
174
+
175
+ before do
176
+ admin = rdkafka_producer_config.admin
177
+ admin.create_topic(topic, 1, 1).wait
178
+ wait_for_topic(admin, topic)
179
+ admin.close
180
+ end
181
+
182
+ it "should raise an error when seeking fails" do
183
+ fake_msg = OpenStruct.new(topic: topic, partition: 0, offset: 0)
184
+
185
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_seek).and_return(20)
186
+ expect {
187
+ consumer.seek(fake_msg)
188
+ }.to raise_error Rdkafka::RdkafkaError
189
+ end
190
+
191
+ context "subscription" do
192
+ let(:timeout) { 1000 }
193
+ # Some specs here test the manual offset commit hence we want to ensure, that we have some
194
+ # offsets in-memory that we can manually commit
195
+ let(:consumer) { rdkafka_consumer_config('auto.commit.interval.ms': 60_000).consumer }
196
+
197
+ before do
198
+ consumer.subscribe(topic)
199
+
200
+ # 1. partitions are assigned
201
+ wait_for_assignment(consumer)
202
+ expect(consumer.assignment).not_to be_empty
203
+
204
+ # 2. eat unrelated messages
205
+ while(consumer.poll(timeout)) do; end
206
+ end
207
+ after { consumer.unsubscribe }
208
+
209
+ def send_one_message(val)
210
+ producer.produce(
211
+ topic: topic,
212
+ payload: "payload #{val}",
213
+ key: "key 1",
214
+ partition: 0
215
+ ).wait
216
+ end
217
+
218
+ it "works when a partition is paused" do
219
+ # 3. get reference message
220
+ send_one_message(:a)
221
+ message1 = consumer.poll(timeout)
222
+ expect(message1&.payload).to eq "payload a"
223
+
224
+ # 4. pause the subscription
225
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
226
+ tpl.add_topic(topic, 1)
227
+ consumer.pause(tpl)
228
+
229
+ # 5. seek to previous message
230
+ consumer.seek(message1)
231
+
232
+ # 6. resume the subscription
233
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
234
+ tpl.add_topic(topic, 1)
235
+ consumer.resume(tpl)
236
+
237
+ # 7. ensure same message is read again
238
+ message2 = consumer.poll(timeout)
239
+
240
+ # This is needed because `enable.auto.offset.store` is true but when running in CI that
241
+ # is overloaded, offset store lags
242
+ sleep(1)
243
+
244
+ consumer.commit
245
+ expect(message1.offset).to eq message2.offset
246
+ expect(message1.payload).to eq message2.payload
247
+ end
248
+
249
+ it "allows skipping messages" do
250
+ # 3. send messages
251
+ send_one_message(:a)
252
+ send_one_message(:b)
253
+ send_one_message(:c)
254
+
255
+ # 4. get reference message
256
+ message = consumer.poll(timeout)
257
+ expect(message&.payload).to eq "payload a"
258
+
259
+ # 5. seek over one message
260
+ fake_msg = message.dup
261
+ fake_msg.instance_variable_set(:@offset, fake_msg.offset + 2)
262
+ consumer.seek(fake_msg)
263
+
264
+ # 6. ensure that only one message is available
265
+ records = consumer.poll(timeout)
266
+ expect(records&.payload).to eq "payload c"
267
+ records = consumer.poll(timeout)
268
+ expect(records).to be_nil
269
+ end
270
+ end
271
+ end
272
+
273
+ describe "#seek_by" do
274
+ let(:consumer) { rdkafka_consumer_config('auto.commit.interval.ms': 60_000).consumer }
275
+ let(:topic) { "it-#{SecureRandom.uuid}" }
276
+ let(:partition) { 0 }
277
+ let(:offset) { 0 }
278
+
279
+ before do
280
+ admin = rdkafka_producer_config.admin
281
+ admin.create_topic(topic, 1, 1).wait
282
+ wait_for_topic(admin, topic)
283
+ admin.close
284
+ end
285
+
286
+ it "should raise an error when seeking fails" do
287
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_seek).and_return(20)
288
+ expect {
289
+ consumer.seek_by(topic, partition, offset)
290
+ }.to raise_error Rdkafka::RdkafkaError
291
+ end
292
+
293
+ context "subscription" do
294
+ let(:timeout) { 1000 }
295
+
296
+ before do
297
+ consumer.subscribe(topic)
298
+
299
+ # 1. partitions are assigned
300
+ wait_for_assignment(consumer)
301
+ expect(consumer.assignment).not_to be_empty
302
+
303
+ # 2. eat unrelated messages
304
+ while(consumer.poll(timeout)) do; end
305
+ end
306
+
307
+ after { consumer.unsubscribe }
308
+
309
+ def send_one_message(val)
310
+ producer.produce(
311
+ topic: topic,
312
+ payload: "payload #{val}",
313
+ key: "key 1",
314
+ partition: 0
315
+ ).wait
316
+ end
317
+
318
+ it "works when a partition is paused" do
319
+ # 3. get reference message
320
+ send_one_message(:a)
321
+ message1 = consumer.poll(timeout)
322
+ expect(message1&.payload).to eq "payload a"
323
+
324
+ # 4. pause the subscription
325
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
326
+ tpl.add_topic(topic, 1)
327
+ consumer.pause(tpl)
328
+
329
+ # 5. seek by the previous message fields
330
+ consumer.seek_by(message1.topic, message1.partition, message1.offset)
331
+
332
+ # 6. resume the subscription
333
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
334
+ tpl.add_topic(topic, 1)
335
+ consumer.resume(tpl)
336
+
337
+ # 7. ensure same message is read again
338
+ message2 = consumer.poll(timeout)
339
+
340
+ # This is needed because `enable.auto.offset.store` is true but when running in CI that
341
+ # is overloaded, offset store lags
342
+ sleep(2)
343
+
344
+ consumer.commit
345
+ expect(message1.offset).to eq message2.offset
346
+ expect(message1.payload).to eq message2.payload
347
+ end
348
+
349
+ it "allows skipping messages" do
350
+ # 3. send messages
351
+ send_one_message(:a)
352
+ send_one_message(:b)
353
+ send_one_message(:c)
354
+
355
+ # 4. get reference message
356
+ message = consumer.poll(timeout)
357
+ expect(message&.payload).to eq "payload a"
358
+
359
+ # 5. seek over one message
360
+ consumer.seek_by(message.topic, message.partition, message.offset + 2)
361
+
362
+ # 6. ensure that only one message is available
363
+ records = consumer.poll(timeout)
364
+ expect(records&.payload).to eq "payload c"
365
+ records = consumer.poll(timeout)
366
+ expect(records).to be_nil
367
+ end
368
+ end
369
+ end
370
+
371
+ describe "#assign and #assignment" do
372
+ it "should return an empty assignment if nothing is assigned" do
373
+ expect(consumer.assignment).to be_empty
374
+ end
375
+
376
+ it "should only accept a topic partition list in assign" do
377
+ expect {
378
+ consumer.assign("list")
379
+ }.to raise_error TypeError
380
+ end
381
+
382
+ it "should raise an error when assigning fails" do
383
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_assign).and_return(20)
384
+ expect {
385
+ consumer.assign(Rdkafka::Consumer::TopicPartitionList.new)
386
+ }.to raise_error Rdkafka::RdkafkaError
387
+ end
388
+
389
+ it "should assign specific topic/partitions and return that assignment" do
390
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
391
+ tpl.add_topic("consume_test_topic", (0..2))
392
+ consumer.assign(tpl)
393
+
394
+ assignment = consumer.assignment
395
+ expect(assignment).not_to be_empty
396
+ expect(assignment.to_h["consume_test_topic"].length).to eq 3
397
+ end
398
+
399
+ it "should return the assignment when subscribed" do
400
+ # Make sure there's a message
401
+ producer.produce(
402
+ topic: "consume_test_topic",
403
+ payload: "payload 1",
404
+ key: "key 1",
405
+ partition: 0
406
+ ).wait
407
+
408
+ # Subscribe and poll until partitions are assigned
409
+ consumer.subscribe("consume_test_topic")
410
+ 100.times do
411
+ consumer.poll(100)
412
+ break unless consumer.assignment.empty?
413
+ end
414
+
415
+ assignment = consumer.assignment
416
+ expect(assignment).not_to be_empty
417
+ expect(assignment.to_h["consume_test_topic"].length).to eq 3
418
+ end
419
+
420
+ it "should raise an error when getting assignment fails" do
421
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_assignment).and_return(20)
422
+ expect {
423
+ consumer.assignment
424
+ }.to raise_error Rdkafka::RdkafkaError
425
+ end
426
+ end
427
+
428
+ describe '#assignment_lost?' do
429
+ it "should not return true as we do have an assignment" do
430
+ consumer.subscribe("consume_test_topic")
431
+ Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
432
+ list.add_topic("consume_test_topic")
433
+ end
434
+
435
+ expect(consumer.assignment_lost?).to eq false
436
+ consumer.unsubscribe
437
+ end
438
+
439
+ it "should not return true after voluntary unsubscribing" do
440
+ consumer.subscribe("consume_test_topic")
441
+ Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
442
+ list.add_topic("consume_test_topic")
443
+ end
444
+
445
+ consumer.unsubscribe
446
+ expect(consumer.assignment_lost?).to eq false
447
+ end
448
+ end
449
+
450
+ describe "#close" do
451
+ it "should close a consumer" do
452
+ consumer.subscribe("consume_test_topic")
453
+ 100.times do |i|
454
+ producer.produce(
455
+ topic: "consume_test_topic",
456
+ payload: "payload #{i}",
457
+ key: "key #{i}",
458
+ partition: 0
459
+ ).wait
460
+ end
461
+ consumer.close
462
+ expect {
463
+ consumer.poll(100)
464
+ }.to raise_error(Rdkafka::ClosedConsumerError, /poll/)
465
+ end
466
+
467
+ context 'when there are outgoing operations in other threads' do
468
+ it 'should wait and not crash' do
469
+ times = []
470
+
471
+ # Run a long running poll
472
+ thread = Thread.new do
473
+ times << Time.now
474
+ consumer.subscribe("empty_test_topic")
475
+ times << Time.now
476
+ consumer.poll(1_000)
477
+ times << Time.now
478
+ end
479
+
480
+ # Make sure it starts before we close
481
+ sleep(0.1)
482
+ consumer.close
483
+ close_time = Time.now
484
+ thread.join
485
+
486
+ times.each { |op_time| expect(op_time).to be < close_time }
487
+ end
488
+ end
489
+ end
490
+
491
+ describe "#position, #commit, #committed and #store_offset" do
492
+ # Make sure there are messages to work with
493
+ let!(:report) do
494
+ producer.produce(
495
+ topic: "consume_test_topic",
496
+ payload: "payload 1",
497
+ key: "key 1",
498
+ partition: 0
499
+ ).wait
500
+ end
501
+
502
+ let(:message) do
503
+ wait_for_message(
504
+ topic: "consume_test_topic",
505
+ delivery_report: report,
506
+ consumer: consumer
507
+ )
508
+ end
509
+
510
+ describe "#position" do
511
+ it "should only accept a topic partition list in position if not nil" do
512
+ expect {
513
+ consumer.position("list")
514
+ }.to raise_error TypeError
515
+ end
516
+ end
517
+
518
+ describe "#committed" do
519
+ it "should only accept a topic partition list in commit if not nil" do
520
+ expect {
521
+ consumer.commit("list")
522
+ }.to raise_error TypeError
523
+ end
524
+
525
+ it "should commit in sync mode" do
526
+ expect {
527
+ consumer.commit(nil, true)
528
+ }.not_to raise_error
529
+ end
530
+ end
531
+
532
+ context "with a committed consumer" do
533
+ before :all do
534
+ # Make sure there are some messages.
535
+ handles = []
536
+ producer = rdkafka_config.producer
537
+ 10.times do
538
+ (0..2).each do |i|
539
+ handles << producer.produce(
540
+ topic: "consume_test_topic",
541
+ payload: "payload 1",
542
+ key: "key 1",
543
+ partition: i
544
+ )
545
+ end
546
+ end
547
+ handles.each(&:wait)
548
+ producer.close
549
+ end
550
+
551
+ before do
552
+ consumer.subscribe("consume_test_topic")
553
+ wait_for_assignment(consumer)
554
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
555
+ list.add_topic_and_partitions_with_offsets("consume_test_topic", 0 => 1, 1 => 1, 2 => 1)
556
+ end
557
+ consumer.commit(list)
558
+ end
559
+
560
+ it "should commit a specific topic partion list" do
561
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
562
+ list.add_topic_and_partitions_with_offsets("consume_test_topic", 0 => 1, 1 => 2, 2 => 3)
563
+ end
564
+ consumer.commit(list)
565
+
566
+ partitions = consumer.committed(list).to_h["consume_test_topic"]
567
+ expect(partitions[0].offset).to eq 1
568
+ expect(partitions[1].offset).to eq 2
569
+ expect(partitions[2].offset).to eq 3
570
+ end
571
+
572
+ it "should raise an error when committing fails" do
573
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_commit).and_return(20)
574
+
575
+ expect {
576
+ consumer.commit
577
+ }.to raise_error(Rdkafka::RdkafkaError)
578
+ end
579
+
580
+ describe "#committed" do
581
+ it "should fetch the committed offsets for the current assignment" do
582
+ partitions = consumer.committed.to_h["consume_test_topic"]
583
+ expect(partitions).not_to be_nil
584
+ expect(partitions[0].offset).to eq 1
585
+ end
586
+
587
+ it "should fetch the committed offsets for a specified topic partition list" do
588
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
589
+ list.add_topic("consume_test_topic", [0, 1, 2])
590
+ end
591
+ partitions = consumer.committed(list).to_h["consume_test_topic"]
592
+ expect(partitions).not_to be_nil
593
+ expect(partitions[0].offset).to eq 1
594
+ expect(partitions[1].offset).to eq 1
595
+ expect(partitions[2].offset).to eq 1
596
+ end
597
+
598
+ it "should raise an error when getting committed fails" do
599
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_committed).and_return(20)
600
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
601
+ list.add_topic("consume_test_topic", [0, 1, 2])
602
+ end
603
+ expect {
604
+ consumer.committed(list)
605
+ }.to raise_error Rdkafka::RdkafkaError
606
+ end
607
+ end
608
+
609
+ describe "#store_offset" do
610
+ let(:consumer) { rdkafka_consumer_config('enable.auto.offset.store': false).consumer }
611
+ let(:metadata) { SecureRandom.uuid }
612
+ let(:group_id) { SecureRandom.uuid }
613
+ let(:base_config) do
614
+ {
615
+ 'group.id': group_id,
616
+ 'enable.auto.offset.store': false,
617
+ 'enable.auto.commit': false
618
+ }
619
+ end
620
+
621
+ before do
622
+ @new_consumer = rdkafka_consumer_config(base_config).consumer
623
+ @new_consumer.subscribe("consume_test_topic")
624
+ wait_for_assignment(@new_consumer)
625
+ end
626
+
627
+ after do
628
+ @new_consumer.close
629
+ end
630
+
631
+ it "should store the offset for a message" do
632
+ @new_consumer.store_offset(message)
633
+ @new_consumer.commit
634
+
635
+ # TODO use position here, should be at offset
636
+
637
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
638
+ list.add_topic("consume_test_topic", [0, 1, 2])
639
+ end
640
+ partitions = @new_consumer.committed(list).to_h["consume_test_topic"]
641
+ expect(partitions).not_to be_nil
642
+ expect(partitions[message.partition].offset).to eq(message.offset + 1)
643
+ end
644
+
645
+ it "should store the offset for a message with metadata" do
646
+ @new_consumer.store_offset(message, metadata)
647
+ @new_consumer.commit
648
+ @new_consumer.close
649
+
650
+ meta_consumer = rdkafka_consumer_config(base_config).consumer
651
+ meta_consumer.subscribe("consume_test_topic")
652
+ wait_for_assignment(meta_consumer)
653
+ meta_consumer.poll(1_000)
654
+ expect(meta_consumer.committed.to_h[message.topic][message.partition].metadata).to eq(metadata)
655
+ meta_consumer.close
656
+ end
657
+
658
+ it "should raise an error with invalid input" do
659
+ allow(message).to receive(:partition).and_return(9999)
660
+ expect {
661
+ @new_consumer.store_offset(message)
662
+ }.to raise_error Rdkafka::RdkafkaError
663
+ end
664
+
665
+ describe "#position" do
666
+ it "should fetch the positions for the current assignment" do
667
+ consumer.store_offset(message)
668
+
669
+ partitions = consumer.position.to_h["consume_test_topic"]
670
+ expect(partitions).not_to be_nil
671
+ expect(partitions[0].offset).to eq message.offset + 1
672
+ end
673
+
674
+ it "should fetch the positions for a specified assignment" do
675
+ consumer.store_offset(message)
676
+
677
+ list = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
678
+ list.add_topic_and_partitions_with_offsets("consume_test_topic", 0 => nil, 1 => nil, 2 => nil)
679
+ end
680
+ partitions = consumer.position(list).to_h["consume_test_topic"]
681
+ expect(partitions).not_to be_nil
682
+ expect(partitions[0].offset).to eq message.offset + 1
683
+ end
684
+
685
+ it "should raise an error when getting the position fails" do
686
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_position).and_return(20)
687
+
688
+ expect {
689
+ consumer.position
690
+ }.to raise_error(Rdkafka::RdkafkaError)
691
+ end
692
+ end
693
+
694
+ context "when trying to use with enable.auto.offset.store set to true" do
695
+ let(:consumer) { rdkafka_consumer_config('enable.auto.offset.store': true).consumer }
696
+
697
+ it "expect to raise invalid configuration error" do
698
+ expect { consumer.store_offset(message) }.to raise_error(Rdkafka::RdkafkaError, /invalid_arg/)
699
+ end
700
+ end
701
+ end
702
+ end
703
+ end
704
+
705
+ describe "#query_watermark_offsets" do
706
+ it "should return the watermark offsets" do
707
+ # Make sure there's a message
708
+ producer.produce(
709
+ topic: "watermarks_test_topic",
710
+ payload: "payload 1",
711
+ key: "key 1",
712
+ partition: 0
713
+ ).wait
714
+
715
+ low, high = consumer.query_watermark_offsets("watermarks_test_topic", 0, 5000)
716
+ expect(low).to eq 0
717
+ expect(high).to be > 0
718
+ end
719
+
720
+ it "should raise an error when querying offsets fails" do
721
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_query_watermark_offsets).and_return(20)
722
+ expect {
723
+ consumer.query_watermark_offsets("consume_test_topic", 0, 5000)
724
+ }.to raise_error Rdkafka::RdkafkaError
725
+ end
726
+ end
727
+
728
+ describe "#lag" do
729
+ let(:consumer) { rdkafka_consumer_config(:"enable.partition.eof" => true).consumer }
730
+
731
+ it "should calculate the consumer lag" do
732
+ # Make sure there's a message in every partition and
733
+ # wait for the message to make sure everything is committed.
734
+ (0..2).each do |i|
735
+ producer.produce(
736
+ topic: "consume_test_topic",
737
+ key: "key lag #{i}",
738
+ partition: i
739
+ ).wait
740
+ end
741
+
742
+ # Consume to the end
743
+ consumer.subscribe("consume_test_topic")
744
+ eof_count = 0
745
+ loop do
746
+ begin
747
+ consumer.poll(100)
748
+ rescue Rdkafka::RdkafkaError => error
749
+ if error.is_partition_eof?
750
+ eof_count += 1
751
+ end
752
+ break if eof_count == 3
753
+ end
754
+ end
755
+
756
+ # Commit
757
+ consumer.commit
758
+
759
+ # Create list to fetch lag for. TODO creating the list will not be necessary
760
+ # after committed uses the subscription.
761
+ list = consumer.committed(Rdkafka::Consumer::TopicPartitionList.new.tap do |l|
762
+ l.add_topic("consume_test_topic", (0..2))
763
+ end)
764
+
765
+ # Lag should be 0 now
766
+ lag = consumer.lag(list)
767
+ expected_lag = {
768
+ "consume_test_topic" => {
769
+ 0 => 0,
770
+ 1 => 0,
771
+ 2 => 0
772
+ }
773
+ }
774
+ expect(lag).to eq(expected_lag)
775
+
776
+ # Produce message on every topic again
777
+ (0..2).each do |i|
778
+ producer.produce(
779
+ topic: "consume_test_topic",
780
+ key: "key lag #{i}",
781
+ partition: i
782
+ ).wait
783
+ end
784
+
785
+ # Lag should be 1 now
786
+ lag = consumer.lag(list)
787
+ expected_lag = {
788
+ "consume_test_topic" => {
789
+ 0 => 1,
790
+ 1 => 1,
791
+ 2 => 1
792
+ }
793
+ }
794
+ expect(lag).to eq(expected_lag)
795
+ end
796
+
797
+ it "returns nil if there are no messages on the topic" do
798
+ list = consumer.committed(Rdkafka::Consumer::TopicPartitionList.new.tap do |l|
799
+ l.add_topic("consume_test_topic", (0..2))
800
+ end)
801
+
802
+ lag = consumer.lag(list)
803
+ expected_lag = {
804
+ "consume_test_topic" => {}
805
+ }
806
+ expect(lag).to eq(expected_lag)
807
+ end
808
+ end
809
+
810
+ describe "#cluster_id" do
811
+ it 'should return the current ClusterId' do
812
+ consumer.subscribe("consume_test_topic")
813
+ wait_for_assignment(consumer)
814
+ expect(consumer.cluster_id).not_to be_empty
815
+ end
816
+ end
817
+
818
+ describe "#member_id" do
819
+ it 'should return the current MemberId' do
820
+ consumer.subscribe("consume_test_topic")
821
+ wait_for_assignment(consumer)
822
+ expect(consumer.member_id).to start_with('rdkafka-')
823
+ end
824
+ end
825
+
826
+ describe "#poll" do
827
+ it "should return nil if there is no subscription" do
828
+ expect(consumer.poll(1000)).to be_nil
829
+ end
830
+
831
+ it "should return nil if there are no messages" do
832
+ consumer.subscribe("empty_test_topic")
833
+ expect(consumer.poll(1000)).to be_nil
834
+ end
835
+
836
+ it "should return a message if there is one" do
837
+ topic = "it-#{SecureRandom.uuid}"
838
+
839
+ producer.produce(
840
+ topic: topic,
841
+ payload: "payload 1",
842
+ key: "key 1"
843
+ ).wait
844
+ consumer.subscribe(topic)
845
+ message = consumer.each {|m| break m}
846
+
847
+ expect(message).to be_a Rdkafka::Consumer::Message
848
+ expect(message.payload).to eq('payload 1')
849
+ expect(message.key).to eq('key 1')
850
+ end
851
+
852
+ it "should raise an error when polling fails" do
853
+ message = Rdkafka::Bindings::Message.new.tap do |message|
854
+ message[:err] = 20
855
+ end
856
+ message_pointer = message.to_ptr
857
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_consumer_poll).and_return(message_pointer)
858
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_message_destroy).with(message_pointer)
859
+ expect {
860
+ consumer.poll(100)
861
+ }.to raise_error Rdkafka::RdkafkaError
862
+ end
863
+
864
+ it "expect to raise error when polling non-existing topic" do
865
+ missing_topic = SecureRandom.uuid
866
+ consumer.subscribe(missing_topic)
867
+
868
+ expect {
869
+ consumer.poll(1_000)
870
+ }.to raise_error Rdkafka::RdkafkaError, /Subscribed topic not available: #{missing_topic}/
871
+ end
872
+ end
873
+
874
+ describe "#poll with headers" do
875
+ it "should return message with headers using string keys (when produced with symbol keys)" do
876
+ report = producer.produce(
877
+ topic: "consume_test_topic",
878
+ key: "key headers",
879
+ headers: { foo: 'bar' }
880
+ ).wait
881
+
882
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
883
+ expect(message).to be
884
+ expect(message.key).to eq('key headers')
885
+ expect(message.headers).to include('foo' => 'bar')
886
+ end
887
+
888
+ it "should return message with headers using string keys (when produced with string keys)" do
889
+ report = producer.produce(
890
+ topic: "consume_test_topic",
891
+ key: "key headers",
892
+ headers: { 'foo' => 'bar' }
893
+ ).wait
894
+
895
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
896
+ expect(message).to be
897
+ expect(message.key).to eq('key headers')
898
+ expect(message.headers).to include('foo' => 'bar')
899
+ end
900
+
901
+ it "should return message with no headers" do
902
+ report = producer.produce(
903
+ topic: "consume_test_topic",
904
+ key: "key no headers",
905
+ headers: nil
906
+ ).wait
907
+
908
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
909
+ expect(message).to be
910
+ expect(message.key).to eq('key no headers')
911
+ expect(message.headers).to be_empty
912
+ end
913
+
914
+ it "should raise an error when message headers aren't readable" do
915
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_message_headers).with(any_args) { 1 }
916
+
917
+ report = producer.produce(
918
+ topic: "consume_test_topic",
919
+ key: "key err headers",
920
+ headers: nil
921
+ ).wait
922
+
923
+ expect {
924
+ wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
925
+ }.to raise_error do |err|
926
+ expect(err).to be_instance_of(Rdkafka::RdkafkaError)
927
+ expect(err.message).to start_with("Error reading message headers")
928
+ end
929
+ end
930
+
931
+ it "should raise an error when the first message header aren't readable" do
932
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_header_get_all).with(any_args) { 1 }
933
+
934
+ report = producer.produce(
935
+ topic: "consume_test_topic",
936
+ key: "key err headers",
937
+ headers: { foo: 'bar' }
938
+ ).wait
939
+
940
+ expect {
941
+ wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
942
+ }.to raise_error do |err|
943
+ expect(err).to be_instance_of(Rdkafka::RdkafkaError)
944
+ expect(err.message).to start_with("Error reading a message header at index 0")
945
+ end
946
+ end
947
+ end
948
+
949
+ describe "#each" do
950
+ it "should yield messages" do
951
+ handles = []
952
+ 10.times do
953
+ handles << producer.produce(
954
+ topic: "consume_test_topic",
955
+ payload: "payload 1",
956
+ key: "key 1",
957
+ partition: 0
958
+ )
959
+ end
960
+ handles.each(&:wait)
961
+
962
+ consumer.subscribe("consume_test_topic")
963
+ # Check the first 10 messages. Then close the consumer, which
964
+ # should break the each loop.
965
+ consumer.each_with_index do |message, i|
966
+ expect(message).to be_a Rdkafka::Consumer::Message
967
+ break if i == 10
968
+ end
969
+ consumer.close
970
+ end
971
+ end
972
+
973
+ describe "#each_batch" do
974
+ it 'expect to raise an error' do
975
+ expect do
976
+ consumer.each_batch {}
977
+ end.to raise_error(NotImplementedError)
978
+ end
979
+ end
980
+
981
+ describe "#offsets_for_times" do
982
+ it "should raise when not TopicPartitionList" do
983
+ expect { consumer.offsets_for_times([]) }.to raise_error(TypeError)
984
+ end
985
+
986
+ it "should raise an error when offsets_for_times fails" do
987
+ tpl = Rdkafka::Consumer::TopicPartitionList.new
988
+
989
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_offsets_for_times).and_return(7)
990
+
991
+ expect { consumer.offsets_for_times(tpl) }.to raise_error(Rdkafka::RdkafkaError)
992
+ end
993
+
994
+ context "when subscribed" do
995
+ let(:timeout) { 1000 }
996
+
997
+ before do
998
+ consumer.subscribe("consume_test_topic")
999
+
1000
+ # 1. partitions are assigned
1001
+ wait_for_assignment(consumer)
1002
+ expect(consumer.assignment).not_to be_empty
1003
+
1004
+ # 2. eat unrelated messages
1005
+ while(consumer.poll(timeout)) do; end
1006
+ end
1007
+
1008
+ after { consumer.unsubscribe }
1009
+
1010
+ def send_one_message(val)
1011
+ producer.produce(
1012
+ topic: "consume_test_topic",
1013
+ payload: "payload #{val}",
1014
+ key: "key 0",
1015
+ partition: 0
1016
+ ).wait
1017
+ end
1018
+
1019
+ it "returns a TopicParticionList with updated offsets" do
1020
+ send_one_message("a")
1021
+ send_one_message("b")
1022
+ send_one_message("c")
1023
+
1024
+ consumer.poll(timeout)
1025
+ message = consumer.poll(timeout)
1026
+ consumer.poll(timeout)
1027
+
1028
+ tpl = Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
1029
+ list.add_topic_and_partitions_with_offsets(
1030
+ "consume_test_topic",
1031
+ [
1032
+ [0, message.timestamp]
1033
+ ]
1034
+ )
1035
+ end
1036
+
1037
+ tpl_response = consumer.offsets_for_times(tpl)
1038
+
1039
+ expect(tpl_response.to_h["consume_test_topic"][0].offset).to eq message.offset
1040
+ end
1041
+ end
1042
+ end
1043
+
1044
+ # Only relevant in case of a consumer with separate queues
1045
+ describe '#events_poll' do
1046
+ let(:stats) { [] }
1047
+
1048
+ before { Rdkafka::Config.statistics_callback = ->(published) { stats << published } }
1049
+
1050
+ after { Rdkafka::Config.statistics_callback = nil }
1051
+
1052
+ let(:consumer) do
1053
+ config = rdkafka_consumer_config('statistics.interval.ms': 500)
1054
+ config.consumer_poll_set = false
1055
+ config.consumer
1056
+ end
1057
+
1058
+ it "expect to run events_poll, operate and propagate stats on events_poll and not poll" do
1059
+ consumer.subscribe("consume_test_topic")
1060
+ consumer.poll(1_000)
1061
+ expect(stats).to be_empty
1062
+ consumer.events_poll(-1)
1063
+ expect(stats).not_to be_empty
1064
+ end
1065
+ end
1066
+
1067
+ describe '#consumer_group_metadata_pointer' do
1068
+ let(:pointer) { consumer.consumer_group_metadata_pointer }
1069
+
1070
+ after { Rdkafka::Bindings.rd_kafka_consumer_group_metadata_destroy(pointer) }
1071
+
1072
+ it 'expect to return a pointer' do
1073
+ expect(pointer).to be_a(FFI::Pointer)
1074
+ end
1075
+ end
1076
+
1077
+ describe "a rebalance listener" do
1078
+ let(:consumer) do
1079
+ config = rdkafka_consumer_config
1080
+ config.consumer_rebalance_listener = listener
1081
+ config.consumer
1082
+ end
1083
+
1084
+ context "with a working listener" do
1085
+ let(:listener) do
1086
+ Struct.new(:queue) do
1087
+ def on_partitions_assigned(list)
1088
+ collect(:assign, list)
1089
+ end
1090
+
1091
+ def on_partitions_revoked(list)
1092
+ collect(:revoke, list)
1093
+ end
1094
+
1095
+ def collect(name, list)
1096
+ partitions = list.to_h.map { |key, values| [key, values.map(&:partition)] }.flatten
1097
+ queue << ([name] + partitions)
1098
+ end
1099
+ end.new([])
1100
+ end
1101
+
1102
+ it "should get notifications" do
1103
+ notify_listener(listener)
1104
+
1105
+ expect(listener.queue).to eq([
1106
+ [:assign, "consume_test_topic", 0, 1, 2],
1107
+ [:revoke, "consume_test_topic", 0, 1, 2]
1108
+ ])
1109
+ end
1110
+ end
1111
+
1112
+ context "with a broken listener" do
1113
+ let(:listener) do
1114
+ Struct.new(:queue) do
1115
+ def on_partitions_assigned(list)
1116
+ queue << :assigned
1117
+ raise 'boom'
1118
+ end
1119
+
1120
+ def on_partitions_revoked(list)
1121
+ queue << :revoked
1122
+ raise 'boom'
1123
+ end
1124
+ end.new([])
1125
+ end
1126
+
1127
+ it 'should handle callback exceptions' do
1128
+ notify_listener(listener)
1129
+
1130
+ expect(listener.queue).to eq([:assigned, :revoked])
1131
+ end
1132
+ end
1133
+ end
1134
+
1135
+ context "methods that should not be called after a consumer has been closed" do
1136
+ before do
1137
+ consumer.close
1138
+ end
1139
+
1140
+ # Affected methods and a non-invalid set of parameters for the method
1141
+ {
1142
+ :subscribe => [ nil ],
1143
+ :unsubscribe => nil,
1144
+ :pause => [ nil ],
1145
+ :resume => [ nil ],
1146
+ :subscription => nil,
1147
+ :assign => [ nil ],
1148
+ :assignment => nil,
1149
+ :committed => [],
1150
+ :query_watermark_offsets => [ nil, nil ],
1151
+ :assignment_lost? => []
1152
+ }.each do |method, args|
1153
+ it "raises an exception if #{method} is called" do
1154
+ expect {
1155
+ if args.nil?
1156
+ consumer.public_send(method)
1157
+ else
1158
+ consumer.public_send(method, *args)
1159
+ end
1160
+ }.to raise_exception(Rdkafka::ClosedConsumerError, /#{method.to_s}/)
1161
+ end
1162
+ end
1163
+ end
1164
+
1165
+ it "provides a finalizer that closes the native kafka client" do
1166
+ expect(consumer.closed?).to eq(false)
1167
+
1168
+ consumer.finalizer.call("some-ignored-object-id")
1169
+
1170
+ expect(consumer.closed?).to eq(true)
1171
+ end
1172
+
1173
+ context "when the rebalance protocol is cooperative" do
1174
+ let(:consumer) do
1175
+ config = rdkafka_consumer_config(
1176
+ {
1177
+ :"partition.assignment.strategy" => "cooperative-sticky",
1178
+ :"debug" => "consumer",
1179
+ }
1180
+ )
1181
+ config.consumer_rebalance_listener = listener
1182
+ config.consumer
1183
+ end
1184
+
1185
+ let(:listener) do
1186
+ Struct.new(:queue) do
1187
+ def on_partitions_assigned(list)
1188
+ collect(:assign, list)
1189
+ end
1190
+
1191
+ def on_partitions_revoked(list)
1192
+ collect(:revoke, list)
1193
+ end
1194
+
1195
+ def collect(name, list)
1196
+ partitions = list.to_h.map { |key, values| [key, values.map(&:partition)] }.flatten
1197
+ queue << ([name] + partitions)
1198
+ end
1199
+ end.new([])
1200
+ end
1201
+
1202
+ it "should be able to assign and unassign partitions using the cooperative partition assignment APIs" do
1203
+ notify_listener(listener) do
1204
+ handles = []
1205
+ 10.times do
1206
+ handles << producer.produce(
1207
+ topic: "consume_test_topic",
1208
+ payload: "payload 1",
1209
+ key: "key 1",
1210
+ partition: 0
1211
+ )
1212
+ end
1213
+ handles.each(&:wait)
1214
+
1215
+ consumer.subscribe("consume_test_topic")
1216
+ # Check the first 10 messages. Then close the consumer, which
1217
+ # should break the each loop.
1218
+ consumer.each_with_index do |message, i|
1219
+ expect(message).to be_a Rdkafka::Consumer::Message
1220
+ break if i == 10
1221
+ end
1222
+ end
1223
+
1224
+ expect(listener.queue).to eq([
1225
+ [:assign, "consume_test_topic", 0, 1, 2],
1226
+ [:revoke, "consume_test_topic", 0, 1, 2]
1227
+ ])
1228
+ end
1229
+ end
1230
+
1231
+ describe '#oauthbearer_set_token' do
1232
+ context 'when sasl not configured' do
1233
+ it 'should return RD_KAFKA_RESP_ERR__STATE' do
1234
+ response = consumer.oauthbearer_set_token(
1235
+ token: "foo",
1236
+ lifetime_ms: Time.now.to_i*1000 + 900 * 1000,
1237
+ principal_name: "kafka-cluster"
1238
+ )
1239
+ expect(response).to eq(Rdkafka::Bindings::RD_KAFKA_RESP_ERR__STATE)
1240
+ end
1241
+ end
1242
+
1243
+ context 'when sasl configured' do
1244
+ before do
1245
+ $consumer_sasl = rdkafka_producer_config(
1246
+ "security.protocol": "sasl_ssl",
1247
+ "sasl.mechanisms": 'OAUTHBEARER'
1248
+ ).consumer
1249
+ end
1250
+
1251
+ after do
1252
+ $consumer_sasl.close
1253
+ end
1254
+
1255
+ it 'should succeed' do
1256
+
1257
+ response = $consumer_sasl.oauthbearer_set_token(
1258
+ token: "foo",
1259
+ lifetime_ms: Time.now.to_i*1000 + 900 * 1000,
1260
+ principal_name: "kafka-cluster"
1261
+ )
1262
+ expect(response).to eq(0)
1263
+ end
1264
+ end
1265
+ end
1266
+
1267
+ describe "when reaching eof on a topic and eof reporting enabled" do
1268
+ let(:consumer) { rdkafka_consumer_config(:"enable.partition.eof" => true).consumer }
1269
+
1270
+ it "should return proper details" do
1271
+ (0..2).each do |i|
1272
+ producer.produce(
1273
+ topic: "consume_test_topic",
1274
+ key: "key lag #{i}",
1275
+ partition: i
1276
+ ).wait
1277
+ end
1278
+
1279
+ # Consume to the end
1280
+ consumer.subscribe("consume_test_topic")
1281
+ eof_error = nil
1282
+
1283
+ loop do
1284
+ begin
1285
+ consumer.poll(100)
1286
+ rescue Rdkafka::RdkafkaError => error
1287
+ if error.is_partition_eof?
1288
+ eof_error = error
1289
+ end
1290
+ break if eof_error
1291
+ end
1292
+ end
1293
+
1294
+ expect(eof_error.code).to eq(:partition_eof)
1295
+ end
1296
+ end
1297
+
1298
+ describe "long running consumption" do
1299
+ let(:consumer) { rdkafka_consumer_config.consumer }
1300
+ let(:producer) { rdkafka_producer_config.producer }
1301
+
1302
+ after { consumer.close }
1303
+ after { producer.close }
1304
+
1305
+ it "should consume messages continuously for 60 seconds" do
1306
+ consumer.subscribe("consume_test_topic")
1307
+ wait_for_assignment(consumer)
1308
+
1309
+ messages_consumed = 0
1310
+ start_time = Time.now
1311
+
1312
+ # Producer thread - sends message every second
1313
+ producer_thread = Thread.new do
1314
+ counter = 0
1315
+ while Time.now - start_time < 60
1316
+ producer.produce(
1317
+ topic: "consume_test_topic",
1318
+ payload: "payload #{counter}",
1319
+ key: "key #{counter}",
1320
+ partition: 0
1321
+ ).wait
1322
+ counter += 1
1323
+ sleep(1)
1324
+ end
1325
+ end
1326
+
1327
+ # Consumer loop
1328
+ while Time.now - start_time < 60
1329
+ message = consumer.poll(1000)
1330
+ if message
1331
+ expect(message).to be_a Rdkafka::Consumer::Message
1332
+ expect(message.topic).to eq("consume_test_topic")
1333
+ messages_consumed += 1
1334
+ consumer.commit if messages_consumed % 10 == 0
1335
+ end
1336
+ end
1337
+
1338
+ producer_thread.join
1339
+
1340
+ expect(messages_consumed).to be > 50 # Should consume most messages
1341
+ end
1342
+ end
1343
+ end