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,1527 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ describe Rdkafka::Producer do
6
+ let(:producer) { rdkafka_producer_config.producer }
7
+ let(:consumer) { rdkafka_consumer_config.consumer }
8
+
9
+ after do
10
+ # Registry should always end up being empty
11
+ registry = Rdkafka::Producer::DeliveryHandle::REGISTRY
12
+ expect(registry).to be_empty, registry.inspect
13
+ producer.close
14
+ consumer.close
15
+ end
16
+
17
+ describe 'producer without auto-start' do
18
+ let(:producer) { rdkafka_producer_config.producer(native_kafka_auto_start: false) }
19
+
20
+ it 'expect to be able to start it later and close' do
21
+ producer.start
22
+ producer.close
23
+ end
24
+
25
+ it 'expect to be able to close it without starting' do
26
+ producer.close
27
+ end
28
+ end
29
+
30
+ describe '#name' do
31
+ it { expect(producer.name).to include('rdkafka#producer-') }
32
+ end
33
+
34
+ describe '#produce with topic config alterations' do
35
+ context 'when config is not valid' do
36
+ it 'expect to raise error' do
37
+ expect do
38
+ producer.produce(topic: 'test', payload: '', topic_config: { 'invalid': 'invalid' })
39
+ end.to raise_error(Rdkafka::Config::ConfigError)
40
+ end
41
+ end
42
+
43
+ context 'when config is valid' do
44
+ it 'expect to raise error' do
45
+ expect do
46
+ producer.produce(topic: 'test', payload: '', topic_config: { 'acks': 1 }).wait
47
+ end.not_to raise_error
48
+ end
49
+
50
+ context 'when alteration should change behavior' do
51
+ # This is set incorrectly for a reason
52
+ # If alteration would not work, this will hang the spec suite
53
+ let(:producer) do
54
+ rdkafka_producer_config(
55
+ 'message.timeout.ms': 1_000_000,
56
+ :"bootstrap.servers" => "127.0.0.1:9094",
57
+ ).producer
58
+ end
59
+
60
+ it 'expect to give up on delivery fast based on alteration config' do
61
+ expect do
62
+ producer.produce(
63
+ topic: 'produce_config_test',
64
+ payload: 'test',
65
+ topic_config: {
66
+ 'compression.type': 'gzip',
67
+ 'message.timeout.ms': 1
68
+ }
69
+ ).wait
70
+ end.to raise_error(Rdkafka::RdkafkaError, /msg_timed_out/)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ context "delivery callback" do
77
+ context "with a proc/lambda" do
78
+ it "should set the callback" do
79
+ expect {
80
+ producer.delivery_callback = lambda do |delivery_handle|
81
+ puts delivery_handle
82
+ end
83
+ }.not_to raise_error
84
+ expect(producer.delivery_callback).to respond_to :call
85
+ end
86
+
87
+ it "should call the callback when a message is delivered" do
88
+ @callback_called = false
89
+
90
+ producer.delivery_callback = lambda do |report|
91
+ expect(report).not_to be_nil
92
+ expect(report.label).to eq "label"
93
+ expect(report.partition).to eq 1
94
+ expect(report.offset).to be >= 0
95
+ expect(report.topic_name).to eq "produce_test_topic"
96
+ @callback_called = true
97
+ end
98
+
99
+ # Produce a message
100
+ handle = producer.produce(
101
+ topic: "produce_test_topic",
102
+ payload: "payload",
103
+ key: "key",
104
+ label: "label"
105
+ )
106
+
107
+ expect(handle.label).to eq "label"
108
+
109
+ # Wait for it to be delivered
110
+ handle.wait(max_wait_timeout: 15)
111
+
112
+ # Join the producer thread.
113
+ producer.close
114
+
115
+ # Callback should have been called
116
+ expect(@callback_called).to be true
117
+ end
118
+
119
+ it "should provide handle" do
120
+ @callback_handle = nil
121
+
122
+ producer.delivery_callback = lambda { |_, handle| @callback_handle = handle }
123
+
124
+ # Produce a message
125
+ handle = producer.produce(
126
+ topic: "produce_test_topic",
127
+ payload: "payload",
128
+ key: "key"
129
+ )
130
+
131
+ # Wait for it to be delivered
132
+ handle.wait(max_wait_timeout: 15)
133
+
134
+ # Join the producer thread.
135
+ producer.close
136
+
137
+ expect(handle).to be @callback_handle
138
+ end
139
+ end
140
+
141
+ context "with a callable object" do
142
+ it "should set the callback" do
143
+ callback = Class.new do
144
+ def call(stats); end
145
+ end
146
+ expect {
147
+ producer.delivery_callback = callback.new
148
+ }.not_to raise_error
149
+ expect(producer.delivery_callback).to respond_to :call
150
+ end
151
+
152
+ it "should call the callback when a message is delivered" do
153
+ called_report = []
154
+ callback = Class.new do
155
+ def initialize(called_report)
156
+ @called_report = called_report
157
+ end
158
+
159
+ def call(report)
160
+ @called_report << report
161
+ end
162
+ end
163
+ producer.delivery_callback = callback.new(called_report)
164
+
165
+ # Produce a message
166
+ handle = producer.produce(
167
+ topic: "produce_test_topic",
168
+ payload: "payload",
169
+ key: "key"
170
+ )
171
+
172
+ # Wait for it to be delivered
173
+ handle.wait(max_wait_timeout: 15)
174
+
175
+ # Join the producer thread.
176
+ producer.close
177
+
178
+ # Callback should have been called
179
+ expect(called_report.first).not_to be_nil
180
+ expect(called_report.first.partition).to eq 1
181
+ expect(called_report.first.offset).to be >= 0
182
+ expect(called_report.first.topic_name).to eq "produce_test_topic"
183
+ end
184
+
185
+ it "should provide handle" do
186
+ callback_handles = []
187
+ callback = Class.new do
188
+ def initialize(callback_handles)
189
+ @callback_handles = callback_handles
190
+ end
191
+
192
+ def call(_, handle)
193
+ @callback_handles << handle
194
+ end
195
+ end
196
+ producer.delivery_callback = callback.new(callback_handles)
197
+
198
+ # Produce a message
199
+ handle = producer.produce(
200
+ topic: "produce_test_topic",
201
+ payload: "payload",
202
+ key: "key"
203
+ )
204
+
205
+ # Wait for it to be delivered
206
+ handle.wait(max_wait_timeout: 15)
207
+
208
+ # Join the producer thread.
209
+ producer.close
210
+
211
+ # Callback should have been called
212
+ expect(handle).to be callback_handles.first
213
+ end
214
+ end
215
+
216
+ it "should not accept a callback that's not callable" do
217
+ expect {
218
+ producer.delivery_callback = 'a string'
219
+ }.to raise_error(TypeError)
220
+ end
221
+ end
222
+
223
+ it "should require a topic" do
224
+ expect {
225
+ producer.produce(
226
+ payload: "payload",
227
+ key: "key"
228
+ )
229
+ }.to raise_error ArgumentError, /missing keyword: [\:]?topic/
230
+ end
231
+
232
+ it "should produce a message" do
233
+ # Produce a message
234
+ handle = producer.produce(
235
+ topic: "produce_test_topic",
236
+ payload: "payload",
237
+ key: "key",
238
+ label: "label"
239
+ )
240
+
241
+ # Should be pending at first
242
+ expect(handle.pending?).to be true
243
+ expect(handle.label).to eq "label"
244
+
245
+ # Check delivery handle and report
246
+ report = handle.wait(max_wait_timeout: 5)
247
+ expect(handle.pending?).to be false
248
+ expect(report).not_to be_nil
249
+ expect(report.partition).to eq 1
250
+ expect(report.offset).to be >= 0
251
+ expect(report.label).to eq "label"
252
+
253
+ # Flush and close producer
254
+ producer.flush
255
+ producer.close
256
+
257
+ # Consume message and verify its content
258
+ message = wait_for_message(
259
+ topic: "produce_test_topic",
260
+ delivery_report: report,
261
+ consumer: consumer
262
+ )
263
+ expect(message.partition).to eq 1
264
+ expect(message.payload).to eq "payload"
265
+ expect(message.key).to eq "key"
266
+ expect(message.timestamp).to be_within(10).of(Time.now)
267
+ end
268
+
269
+ it "should produce a message with a specified partition" do
270
+ # Produce a message
271
+ handle = producer.produce(
272
+ topic: "produce_test_topic",
273
+ payload: "payload partition",
274
+ key: "key partition",
275
+ partition: 1
276
+ )
277
+ report = handle.wait(max_wait_timeout: 5)
278
+
279
+ # Consume message and verify its content
280
+ message = wait_for_message(
281
+ topic: "produce_test_topic",
282
+ delivery_report: report,
283
+ consumer: consumer
284
+ )
285
+ expect(message.partition).to eq 1
286
+ expect(message.key).to eq "key partition"
287
+ end
288
+
289
+ it "should produce a message to the same partition with a similar partition key" do
290
+ # Avoid partitioner collisions.
291
+ while true
292
+ key = ('a'..'z').to_a.shuffle.take(10).join('')
293
+ partition_key = ('a'..'z').to_a.shuffle.take(10).join('')
294
+ partition_count = producer.partition_count('partitioner_test_topic')
295
+ break if (Zlib.crc32(key) % partition_count) != (Zlib.crc32(partition_key) % partition_count)
296
+ end
297
+
298
+ # Produce a message with key, partition_key and key + partition_key
299
+ messages = [{key: key}, {partition_key: partition_key}, {key: key, partition_key: partition_key}]
300
+
301
+ messages = messages.map do |m|
302
+ handle = producer.produce(
303
+ topic: "partitioner_test_topic",
304
+ payload: "payload partition",
305
+ key: m[:key],
306
+ partition_key: m[:partition_key]
307
+ )
308
+ report = handle.wait(max_wait_timeout: 5)
309
+
310
+ wait_for_message(
311
+ topic: "partitioner_test_topic",
312
+ delivery_report: report,
313
+ )
314
+ end
315
+
316
+ expect(messages[0].partition).not_to eq(messages[2].partition)
317
+ expect(messages[1].partition).to eq(messages[2].partition)
318
+ expect(messages[0].key).to eq key
319
+ expect(messages[1].key).to be_nil
320
+ expect(messages[2].key).to eq key
321
+ end
322
+
323
+ it "should produce a message with empty string without crashing" do
324
+ messages = [{key: 'a', partition_key: ''}]
325
+
326
+ messages = messages.map do |m|
327
+ handle = producer.produce(
328
+ topic: "partitioner_test_topic",
329
+ payload: "payload partition",
330
+ key: m[:key],
331
+ partition_key: m[:partition_key]
332
+ )
333
+ report = handle.wait(max_wait_timeout: 5)
334
+
335
+ wait_for_message(
336
+ topic: "partitioner_test_topic",
337
+ delivery_report: report,
338
+ )
339
+ end
340
+
341
+ expect(messages[0].partition).to be >= 0
342
+ expect(messages[0].key).to eq 'a'
343
+ end
344
+
345
+ it "should produce a message with utf-8 encoding" do
346
+ handle = producer.produce(
347
+ topic: "produce_test_topic",
348
+ payload: "Τη γλώσσα μου έδωσαν ελληνική",
349
+ key: "key utf8"
350
+ )
351
+ report = handle.wait(max_wait_timeout: 5)
352
+
353
+ # Consume message and verify its content
354
+ message = wait_for_message(
355
+ topic: "produce_test_topic",
356
+ delivery_report: report,
357
+ consumer: consumer
358
+ )
359
+
360
+ expect(message.partition).to eq 1
361
+ expect(message.payload.force_encoding("utf-8")).to eq "Τη γλώσσα μου έδωσαν ελληνική"
362
+ expect(message.key).to eq "key utf8"
363
+ end
364
+
365
+ it "should produce a message to a non-existing topic with key and partition key" do
366
+ new_topic = "it-#{SecureRandom.uuid}"
367
+
368
+ handle = producer.produce(
369
+ # Needs to be a new topic each time
370
+ topic: new_topic,
371
+ payload: "payload",
372
+ key: "key",
373
+ partition_key: "partition_key",
374
+ label: "label"
375
+ )
376
+
377
+ # Should be pending at first
378
+ expect(handle.pending?).to be true
379
+ expect(handle.label).to eq "label"
380
+
381
+ # Check delivery handle and report
382
+ report = handle.wait(max_wait_timeout: 5)
383
+ expect(handle.pending?).to be false
384
+ expect(report).not_to be_nil
385
+ expect(report.partition).to eq 0
386
+ expect(report.offset).to be >= 0
387
+ expect(report.label).to eq "label"
388
+
389
+ # Flush and close producer
390
+ producer.flush
391
+ producer.close
392
+
393
+ # Consume message and verify its content
394
+ message = wait_for_message(
395
+ topic: new_topic,
396
+ delivery_report: report,
397
+ consumer: consumer
398
+ )
399
+ expect(message.partition).to eq 0
400
+ expect(message.payload).to eq "payload"
401
+ expect(message.key).to eq "key"
402
+ # Since api.version.request is on by default we will get
403
+ # the message creation timestamp if it's not set.
404
+ expect(message.timestamp).to be_within(10).of(Time.now)
405
+ end
406
+
407
+ context "timestamp" do
408
+ it "should raise a type error if not nil, integer or time" do
409
+ expect {
410
+ producer.produce(
411
+ topic: "produce_test_topic",
412
+ payload: "payload timestamp",
413
+ key: "key timestamp",
414
+ timestamp: "10101010"
415
+ )
416
+ }.to raise_error TypeError
417
+ end
418
+
419
+ it "should produce a message with an integer timestamp" do
420
+ handle = producer.produce(
421
+ topic: "produce_test_topic",
422
+ payload: "payload timestamp",
423
+ key: "key timestamp",
424
+ timestamp: 1505069646252
425
+ )
426
+ report = handle.wait(max_wait_timeout: 5)
427
+
428
+ # Consume message and verify its content
429
+ message = wait_for_message(
430
+ topic: "produce_test_topic",
431
+ delivery_report: report,
432
+ consumer: consumer
433
+ )
434
+
435
+ expect(message.partition).to eq 2
436
+ expect(message.key).to eq "key timestamp"
437
+ expect(message.timestamp).to eq Time.at(1505069646, 252_000)
438
+ end
439
+
440
+ it "should produce a message with a time timestamp" do
441
+ handle = producer.produce(
442
+ topic: "produce_test_topic",
443
+ payload: "payload timestamp",
444
+ key: "key timestamp",
445
+ timestamp: Time.at(1505069646, 353_000)
446
+ )
447
+ report = handle.wait(max_wait_timeout: 5)
448
+
449
+ # Consume message and verify its content
450
+ message = wait_for_message(
451
+ topic: "produce_test_topic",
452
+ delivery_report: report,
453
+ consumer: consumer
454
+ )
455
+
456
+ expect(message.partition).to eq 2
457
+ expect(message.key).to eq "key timestamp"
458
+ expect(message.timestamp).to eq Time.at(1505069646, 353_000)
459
+ end
460
+ end
461
+
462
+ it "should produce a message with nil key" do
463
+ handle = producer.produce(
464
+ topic: "produce_test_topic",
465
+ payload: "payload no key"
466
+ )
467
+ report = handle.wait(max_wait_timeout: 5)
468
+
469
+ # Consume message and verify its content
470
+ message = wait_for_message(
471
+ topic: "produce_test_topic",
472
+ delivery_report: report,
473
+ consumer: consumer
474
+ )
475
+
476
+ expect(message.key).to be_nil
477
+ expect(message.payload).to eq "payload no key"
478
+ end
479
+
480
+ it "should produce a message with nil payload" do
481
+ handle = producer.produce(
482
+ topic: "produce_test_topic",
483
+ key: "key no payload"
484
+ )
485
+ report = handle.wait(max_wait_timeout: 5)
486
+
487
+ # Consume message and verify its content
488
+ message = wait_for_message(
489
+ topic: "produce_test_topic",
490
+ delivery_report: report,
491
+ consumer: consumer
492
+ )
493
+
494
+ expect(message.key).to eq "key no payload"
495
+ expect(message.payload).to be_nil
496
+ end
497
+
498
+ it "should produce a message with headers" do
499
+ handle = producer.produce(
500
+ topic: "produce_test_topic",
501
+ payload: "payload headers",
502
+ key: "key headers",
503
+ headers: { foo: :bar, baz: :foobar }
504
+ )
505
+ report = handle.wait(max_wait_timeout: 5)
506
+
507
+ # Consume message and verify its content
508
+ message = wait_for_message(
509
+ topic: "produce_test_topic",
510
+ delivery_report: report,
511
+ consumer: consumer
512
+ )
513
+
514
+ expect(message.payload).to eq "payload headers"
515
+ expect(message.key).to eq "key headers"
516
+ expect(message.headers["foo"]).to eq "bar"
517
+ expect(message.headers["baz"]).to eq "foobar"
518
+ expect(message.headers["foobar"]).to be_nil
519
+ end
520
+
521
+ it "should produce a message with empty headers" do
522
+ handle = producer.produce(
523
+ topic: "produce_test_topic",
524
+ payload: "payload headers",
525
+ key: "key headers",
526
+ headers: {}
527
+ )
528
+ report = handle.wait(max_wait_timeout: 5)
529
+
530
+ # Consume message and verify its content
531
+ message = wait_for_message(
532
+ topic: "produce_test_topic",
533
+ delivery_report: report,
534
+ consumer: consumer
535
+ )
536
+
537
+ expect(message.payload).to eq "payload headers"
538
+ expect(message.key).to eq "key headers"
539
+ expect(message.headers).to be_empty
540
+ end
541
+
542
+ it "should produce message that aren't waited for and not crash" do
543
+ 5.times do
544
+ 200.times do
545
+ producer.produce(
546
+ topic: "produce_test_topic",
547
+ payload: "payload not waiting",
548
+ key: "key not waiting"
549
+ )
550
+ end
551
+
552
+ # Allow some time for a GC run
553
+ sleep 1
554
+ end
555
+
556
+ # Wait for the delivery notifications
557
+ 10.times do
558
+ break if Rdkafka::Producer::DeliveryHandle::REGISTRY.empty?
559
+ sleep 1
560
+ end
561
+ end
562
+
563
+ it "should produce a message in a forked process", skip: defined?(JRUBY_VERSION) && "Kernel#fork is not available" do
564
+ # Fork, produce a message, send the report over a pipe and
565
+ # wait for and check the message in the main process.
566
+ reader, writer = IO.pipe
567
+
568
+ pid = fork do
569
+ reader.close
570
+
571
+ # Avoid sharing the client between processes.
572
+ producer = rdkafka_producer_config.producer
573
+
574
+ handle = producer.produce(
575
+ topic: "produce_test_topic",
576
+ payload: "payload-forked",
577
+ key: "key-forked"
578
+ )
579
+
580
+ report = handle.wait(max_wait_timeout: 5)
581
+
582
+ report_json = JSON.generate(
583
+ "partition" => report.partition,
584
+ "offset" => report.offset,
585
+ "topic_name" => report.topic_name
586
+ )
587
+
588
+ writer.write(report_json)
589
+ writer.close
590
+ producer.flush
591
+ producer.close
592
+ end
593
+ Process.wait(pid)
594
+
595
+ writer.close
596
+ report_hash = JSON.parse(reader.read)
597
+ report = Rdkafka::Producer::DeliveryReport.new(
598
+ report_hash["partition"],
599
+ report_hash["offset"],
600
+ report_hash["topic_name"]
601
+ )
602
+
603
+ reader.close
604
+
605
+ # Consume message and verify its content
606
+ message = wait_for_message(
607
+ topic: "produce_test_topic",
608
+ delivery_report: report,
609
+ consumer: consumer
610
+ )
611
+ expect(message.partition).to eq 0
612
+ expect(message.payload).to eq "payload-forked"
613
+ expect(message.key).to eq "key-forked"
614
+ end
615
+
616
+ it "should raise an error when producing fails" do
617
+ expect(Rdkafka::Bindings).to receive(:rd_kafka_producev).and_return(20)
618
+
619
+ expect {
620
+ producer.produce(
621
+ topic: "produce_test_topic",
622
+ key: "key error"
623
+ )
624
+ }.to raise_error Rdkafka::RdkafkaError
625
+ end
626
+
627
+ it "should raise a timeout error when waiting too long" do
628
+ handle = producer.produce(
629
+ topic: "produce_test_topic",
630
+ payload: "payload timeout",
631
+ key: "key timeout"
632
+ )
633
+ expect {
634
+ handle.wait(max_wait_timeout: 0)
635
+ }.to raise_error Rdkafka::Producer::DeliveryHandle::WaitTimeoutError
636
+
637
+ # Waiting a second time should work
638
+ handle.wait(max_wait_timeout: 5)
639
+ end
640
+
641
+ context "methods that should not be called after a producer has been closed" do
642
+ before do
643
+ producer.close
644
+ end
645
+
646
+ # Affected methods and a non-invalid set of parameters for the method
647
+ {
648
+ :produce => { topic: nil },
649
+ :partition_count => nil,
650
+ }.each do |method, args|
651
+ it "raises an exception if #{method} is called" do
652
+ expect {
653
+ if args.is_a?(Hash)
654
+ producer.public_send(method, **args)
655
+ else
656
+ producer.public_send(method, args)
657
+ end
658
+ }.to raise_exception(Rdkafka::ClosedProducerError, /#{method.to_s}/)
659
+ end
660
+ end
661
+ end
662
+
663
+ context "when not being able to deliver the message" do
664
+ let(:producer) do
665
+ rdkafka_producer_config(
666
+ "bootstrap.servers": "127.0.0.1:9095",
667
+ "message.timeout.ms": 100
668
+ ).producer
669
+ end
670
+
671
+ it "should contain the error in the response when not deliverable" do
672
+ handler = producer.produce(topic: 'produce_test_topic', payload: nil, label: 'na')
673
+ # Wait for the async callbacks and delivery registry to update
674
+ sleep(2)
675
+ expect(handler.create_result.error).to be_a(Rdkafka::RdkafkaError)
676
+ expect(handler.create_result.label).to eq('na')
677
+ end
678
+ end
679
+
680
+ context "when topic does not exist and allow.auto.create.topics is false" do
681
+ let(:producer) do
682
+ rdkafka_producer_config(
683
+ "bootstrap.servers": "127.0.0.1:9092",
684
+ "message.timeout.ms": 100,
685
+ "allow.auto.create.topics": false
686
+ ).producer
687
+ end
688
+
689
+ it "should contain the error in the response when not deliverable" do
690
+ handler = producer.produce(topic: "it-#{SecureRandom.uuid}", payload: nil, label: 'na')
691
+ # Wait for the async callbacks and delivery registry to update
692
+ sleep(2)
693
+ expect(handler.create_result.error).to be_a(Rdkafka::RdkafkaError)
694
+ expect(handler.create_result.error.code).to eq(:msg_timed_out)
695
+ expect(handler.create_result.label).to eq('na')
696
+ end
697
+ end
698
+
699
+ describe '#partition_count' do
700
+ it { expect(producer.partition_count('example_topic')).to eq(1) }
701
+
702
+ context 'when the partition count value is already cached' do
703
+ before do
704
+ producer.partition_count('example_topic')
705
+ allow(::Rdkafka::Metadata).to receive(:new).and_call_original
706
+ end
707
+
708
+ it 'expect not to query it again' do
709
+ producer.partition_count('example_topic')
710
+ expect(::Rdkafka::Metadata).not_to have_received(:new)
711
+ end
712
+ end
713
+
714
+ context 'when the partition count value was cached but time expired' do
715
+ before do
716
+ ::Rdkafka::Producer.partitions_count_cache = Rdkafka::Producer::PartitionsCountCache.new
717
+ allow(::Rdkafka::Metadata).to receive(:new).and_call_original
718
+ end
719
+
720
+ it 'expect to query it again' do
721
+ producer.partition_count('example_topic')
722
+ expect(::Rdkafka::Metadata).to have_received(:new)
723
+ end
724
+ end
725
+
726
+ context 'when the partition count value was cached and time did not expire' do
727
+ before do
728
+ allow(::Process).to receive(:clock_gettime).and_return(0, 29.001)
729
+ producer.partition_count('example_topic')
730
+ allow(::Rdkafka::Metadata).to receive(:new).and_call_original
731
+ end
732
+
733
+ it 'expect not to query it again' do
734
+ producer.partition_count('example_topic')
735
+ expect(::Rdkafka::Metadata).not_to have_received(:new)
736
+ end
737
+ end
738
+ end
739
+
740
+ describe 'metadata fetch request recovery' do
741
+ subject(:partition_count) { producer.partition_count('example_topic') }
742
+
743
+ describe 'metadata initialization recovery' do
744
+ context 'when all good' do
745
+ it { expect(partition_count).to eq(1) }
746
+ end
747
+
748
+ context 'when we fail for the first time with handled error' do
749
+ before do
750
+ raised = false
751
+
752
+ allow(Rdkafka::Bindings).to receive(:rd_kafka_metadata).and_wrap_original do |m, *args|
753
+ if raised
754
+ m.call(*args)
755
+ else
756
+ raised = true
757
+ -185
758
+ end
759
+ end
760
+ end
761
+
762
+ it { expect(partition_count).to eq(1) }
763
+ end
764
+ end
765
+ end
766
+
767
+ describe '#flush' do
768
+ it "should return flush when it can flush all outstanding messages or when no messages" do
769
+ producer.produce(
770
+ topic: "produce_test_topic",
771
+ payload: "payload headers",
772
+ key: "key headers",
773
+ headers: {}
774
+ )
775
+
776
+ expect(producer.flush(5_000)).to eq(true)
777
+ end
778
+
779
+ context 'when it cannot flush due to a timeout' do
780
+ let(:producer) do
781
+ rdkafka_producer_config(
782
+ "bootstrap.servers": "127.0.0.1:9095",
783
+ "message.timeout.ms": 2_000
784
+ ).producer
785
+ end
786
+
787
+ after do
788
+ # Allow rdkafka to evict message preventing memory-leak
789
+ # We give it a bit more time as on slow CIs things take time
790
+ sleep(5)
791
+ end
792
+
793
+ it "should return false on flush when cannot deliver and beyond timeout" do
794
+ producer.produce(
795
+ topic: "produce_test_topic",
796
+ payload: "payload headers",
797
+ key: "key headers",
798
+ headers: {}
799
+ )
800
+
801
+ expect(producer.flush(1_000)).to eq(false)
802
+ end
803
+ end
804
+
805
+ context 'when there is a different error' do
806
+ before { allow(Rdkafka::Bindings).to receive(:rd_kafka_flush).and_return(-199) }
807
+
808
+ it 'should raise it' do
809
+ expect { producer.flush }.to raise_error(Rdkafka::RdkafkaError)
810
+ end
811
+ end
812
+ end
813
+
814
+ describe '#purge' do
815
+ context 'when no outgoing messages' do
816
+ it { expect(producer.purge).to eq(true) }
817
+ end
818
+
819
+ context 'when librdkafka purge returns an error' do
820
+ before { expect(Rdkafka::Bindings).to receive(:rd_kafka_purge).and_return(-153) }
821
+
822
+ it 'expect to raise an error' do
823
+ expect { producer.purge }.to raise_error(Rdkafka::RdkafkaError, /retry/)
824
+ end
825
+ end
826
+
827
+ context 'when there are outgoing things in the queue' do
828
+ let(:producer) do
829
+ rdkafka_producer_config(
830
+ "bootstrap.servers": "127.0.0.1:9095",
831
+ "message.timeout.ms": 2_000
832
+ ).producer
833
+ end
834
+
835
+ it "should should purge and move forward" do
836
+ producer.produce(
837
+ topic: "produce_test_topic",
838
+ payload: "payload headers"
839
+ )
840
+
841
+ expect(producer.purge).to eq(true)
842
+ expect(producer.flush(1_000)).to eq(true)
843
+ end
844
+
845
+ it "should materialize the delivery handles" do
846
+ handle = producer.produce(
847
+ topic: "produce_test_topic",
848
+ payload: "payload headers"
849
+ )
850
+
851
+ expect(producer.purge).to eq(true)
852
+
853
+ expect { handle.wait }.to raise_error(Rdkafka::RdkafkaError, /purge_queue/)
854
+ end
855
+
856
+ context "when using delivery_callback" do
857
+ let(:delivery_reports) { [] }
858
+
859
+ let(:delivery_callback) do
860
+ ->(delivery_report) { delivery_reports << delivery_report }
861
+ end
862
+
863
+ before { producer.delivery_callback = delivery_callback }
864
+
865
+ it "should run the callback" do
866
+ producer.produce(
867
+ topic: "produce_test_topic",
868
+ payload: "payload headers"
869
+ )
870
+
871
+ expect(producer.purge).to eq(true)
872
+ # queue purge
873
+ expect(delivery_reports[0].error).to eq(-152)
874
+ end
875
+ end
876
+ end
877
+ end
878
+
879
+ context 'when working with transactions' do
880
+ let(:producer) do
881
+ rdkafka_producer_config(
882
+ 'transactional.id': SecureRandom.uuid,
883
+ 'transaction.timeout.ms': 5_000
884
+ ).producer
885
+ end
886
+
887
+ it 'expect not to allow to produce without transaction init' do
888
+ expect do
889
+ producer.produce(topic: 'produce_test_topic', payload: 'data')
890
+ end.to raise_error(Rdkafka::RdkafkaError, /Erroneous state \(state\)/)
891
+ end
892
+
893
+ it 'expect to raise error when transactions are initialized but producing not in one' do
894
+ producer.init_transactions
895
+
896
+ expect do
897
+ producer.produce(topic: 'produce_test_topic', payload: 'data')
898
+ end.to raise_error(Rdkafka::RdkafkaError, /Erroneous state \(state\)/)
899
+ end
900
+
901
+ it 'expect to allow to produce within a transaction, finalize and ship data' do
902
+ producer.init_transactions
903
+ producer.begin_transaction
904
+ handle1 = producer.produce(topic: 'produce_test_topic', payload: 'data1', partition: 1)
905
+ handle2 = producer.produce(topic: 'example_topic', payload: 'data2', partition: 0)
906
+ producer.commit_transaction
907
+
908
+ report1 = handle1.wait(max_wait_timeout: 15)
909
+ report2 = handle2.wait(max_wait_timeout: 15)
910
+
911
+ message1 = wait_for_message(
912
+ topic: "produce_test_topic",
913
+ delivery_report: report1,
914
+ consumer: consumer
915
+ )
916
+
917
+ expect(message1.partition).to eq 1
918
+ expect(message1.payload).to eq "data1"
919
+ expect(message1.timestamp).to be_within(10).of(Time.now)
920
+
921
+ message2 = wait_for_message(
922
+ topic: "example_topic",
923
+ delivery_report: report2,
924
+ consumer: consumer
925
+ )
926
+
927
+ expect(message2.partition).to eq 0
928
+ expect(message2.payload).to eq "data2"
929
+ expect(message2.timestamp).to be_within(10).of(Time.now)
930
+ end
931
+
932
+ it 'expect not to send data and propagate purge queue error on abort' do
933
+ producer.init_transactions
934
+ producer.begin_transaction
935
+ handle1 = producer.produce(topic: 'produce_test_topic', payload: 'data1', partition: 1)
936
+ handle2 = producer.produce(topic: 'example_topic', payload: 'data2', partition: 0)
937
+ producer.abort_transaction
938
+
939
+ expect { handle1.wait(max_wait_timeout: 15) }
940
+ .to raise_error(Rdkafka::RdkafkaError, /Purged in queue \(purge_queue\)/)
941
+ expect { handle2.wait(max_wait_timeout: 15) }
942
+ .to raise_error(Rdkafka::RdkafkaError, /Purged in queue \(purge_queue\)/)
943
+ end
944
+
945
+ it 'expect to have non retryable, non abortable and not fatal error on abort' do
946
+ producer.init_transactions
947
+ producer.begin_transaction
948
+ handle = producer.produce(topic: 'produce_test_topic', payload: 'data1', partition: 1)
949
+ producer.abort_transaction
950
+
951
+ response = handle.wait(raise_response_error: false)
952
+
953
+ expect(response.error).to be_a(Rdkafka::RdkafkaError)
954
+ expect(response.error.retryable?).to eq(false)
955
+ expect(response.error.fatal?).to eq(false)
956
+ expect(response.error.abortable?).to eq(false)
957
+ end
958
+
959
+ context 'fencing against previous active producer with same transactional id' do
960
+ let(:transactional_id) { SecureRandom.uuid }
961
+
962
+ let(:producer1) do
963
+ rdkafka_producer_config(
964
+ 'transactional.id': transactional_id,
965
+ 'transaction.timeout.ms': 10_000
966
+ ).producer
967
+ end
968
+
969
+ let(:producer2) do
970
+ rdkafka_producer_config(
971
+ 'transactional.id': transactional_id,
972
+ 'transaction.timeout.ms': 10_000
973
+ ).producer
974
+ end
975
+
976
+ after do
977
+ producer1.close
978
+ producer2.close
979
+ end
980
+
981
+ it 'expect older producer not to be able to commit when fanced out' do
982
+ producer1.init_transactions
983
+ producer1.begin_transaction
984
+ producer1.produce(topic: 'produce_test_topic', payload: 'data1', partition: 1)
985
+
986
+ producer2.init_transactions
987
+ producer2.begin_transaction
988
+ producer2.produce(topic: 'produce_test_topic', payload: 'data1', partition: 1)
989
+
990
+ expect { producer1.commit_transaction }
991
+ .to raise_error(Rdkafka::RdkafkaError, /This instance has been fenced/)
992
+
993
+ error = false
994
+
995
+ begin
996
+ producer1.commit_transaction
997
+ rescue Rdkafka::RdkafkaError => e
998
+ error = e
999
+ end
1000
+
1001
+ expect(error.fatal?).to eq(true)
1002
+ expect(error.abortable?).to eq(false)
1003
+ expect(error.retryable?).to eq(false)
1004
+
1005
+ expect { producer2.commit_transaction }.not_to raise_error
1006
+ end
1007
+ end
1008
+
1009
+ context 'when having a consumer with tpls for exactly once semantics' do
1010
+ let(:tpl) do
1011
+ producer.produce(topic: 'consume_test_topic', payload: 'data1', partition: 0).wait
1012
+ result = producer.produce(topic: 'consume_test_topic', payload: 'data1', partition: 0).wait
1013
+
1014
+ Rdkafka::Consumer::TopicPartitionList.new.tap do |list|
1015
+ list.add_topic_and_partitions_with_offsets("consume_test_topic", 0 => result.offset + 1)
1016
+ end
1017
+ end
1018
+
1019
+ before do
1020
+ consumer.subscribe("consume_test_topic")
1021
+ wait_for_assignment(consumer)
1022
+ producer.init_transactions
1023
+ producer.begin_transaction
1024
+ end
1025
+
1026
+ after { consumer.unsubscribe }
1027
+
1028
+ it 'expect to store offsets and not crash' do
1029
+ producer.send_offsets_to_transaction(consumer, tpl)
1030
+ producer.commit_transaction
1031
+ end
1032
+ end
1033
+ end
1034
+
1035
+ describe '#oauthbearer_set_token' do
1036
+ context 'when sasl not configured' do
1037
+ it 'should return RD_KAFKA_RESP_ERR__STATE' do
1038
+ response = producer.oauthbearer_set_token(
1039
+ token: "foo",
1040
+ lifetime_ms: Time.now.to_i*1000 + 900 * 1000,
1041
+ principal_name: "kafka-cluster"
1042
+ )
1043
+ expect(response).to eq(Rdkafka::Bindings::RD_KAFKA_RESP_ERR__STATE)
1044
+ end
1045
+ end
1046
+
1047
+ context 'when sasl configured' do
1048
+ it 'should succeed' do
1049
+ producer_sasl = rdkafka_producer_config(
1050
+ {
1051
+ "security.protocol": "sasl_ssl",
1052
+ "sasl.mechanisms": 'OAUTHBEARER'
1053
+ }
1054
+ ).producer
1055
+ response = producer_sasl.oauthbearer_set_token(
1056
+ token: "foo",
1057
+ lifetime_ms: Time.now.to_i*1000 + 900 * 1000,
1058
+ principal_name: "kafka-cluster"
1059
+ )
1060
+ expect(response).to eq(0)
1061
+ end
1062
+ end
1063
+ end
1064
+
1065
+ describe "#produce with headers" do
1066
+ it "should produce a message with array headers" do
1067
+ headers = {
1068
+ "version" => ["2.1.3", "2.1.4"],
1069
+ "type" => "String"
1070
+ }
1071
+
1072
+ report = producer.produce(
1073
+ topic: "consume_test_topic",
1074
+ key: "key headers",
1075
+ headers: headers
1076
+ ).wait
1077
+
1078
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
1079
+ expect(message).to be
1080
+ expect(message.key).to eq('key headers')
1081
+ expect(message.headers['type']).to eq('String')
1082
+ expect(message.headers['version']).to eq(["2.1.3", "2.1.4"])
1083
+ end
1084
+
1085
+ it "should produce a message with single value headers" do
1086
+ headers = {
1087
+ "version" => "2.1.3",
1088
+ "type" => "String"
1089
+ }
1090
+
1091
+ report = producer.produce(
1092
+ topic: "consume_test_topic",
1093
+ key: "key headers",
1094
+ headers: headers
1095
+ ).wait
1096
+
1097
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
1098
+ expect(message).to be
1099
+ expect(message.key).to eq('key headers')
1100
+ expect(message.headers['type']).to eq('String')
1101
+ expect(message.headers['version']).to eq('2.1.3')
1102
+ end
1103
+ end
1104
+
1105
+ describe 'with active statistics callback' do
1106
+ let(:producer) do
1107
+ rdkafka_producer_config('statistics.interval.ms': 1_000).producer
1108
+ end
1109
+
1110
+ let(:count_cache_hash) { described_class.partitions_count_cache.to_h }
1111
+ let(:pre_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
1112
+ let(:post_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
1113
+
1114
+ context "when using partition key" do
1115
+ before do
1116
+ Rdkafka::Config.statistics_callback = ->(*) {}
1117
+
1118
+ # This call will make a blocking request to the metadata cache
1119
+ producer.produce(
1120
+ topic: "produce_test_topic",
1121
+ payload: "payload headers",
1122
+ partition_key: "test"
1123
+ ).wait
1124
+
1125
+ pre_statistics_ttl
1126
+
1127
+ # We wait to make sure that statistics are triggered and that there is a refresh
1128
+ sleep(1.5)
1129
+
1130
+ post_statistics_ttl
1131
+ end
1132
+
1133
+ it 'expect to update ttl on the partitions count cache via statistics' do
1134
+ expect(pre_statistics_ttl).to be < post_statistics_ttl
1135
+ end
1136
+ end
1137
+
1138
+ context "when not using partition key" do
1139
+ before do
1140
+ Rdkafka::Config.statistics_callback = ->(*) {}
1141
+
1142
+ # This call will make a blocking request to the metadata cache
1143
+ producer.produce(
1144
+ topic: "produce_test_topic",
1145
+ payload: "payload headers"
1146
+ ).wait
1147
+
1148
+ pre_statistics_ttl
1149
+
1150
+ # We wait to make sure that statistics are triggered and that there is a refresh
1151
+ sleep(1.5)
1152
+
1153
+ # This will anyhow be populated from statistic
1154
+ post_statistics_ttl
1155
+ end
1156
+
1157
+ it 'expect not to update ttl on the partitions count cache via blocking but via use stats' do
1158
+ expect(pre_statistics_ttl).to be_nil
1159
+ expect(post_statistics_ttl).not_to be_nil
1160
+ end
1161
+ end
1162
+ end
1163
+
1164
+ describe 'without active statistics callback' do
1165
+ let(:producer) do
1166
+ rdkafka_producer_config('statistics.interval.ms': 1_000).producer
1167
+ end
1168
+
1169
+ let(:count_cache_hash) { described_class.partitions_count_cache.to_h }
1170
+ let(:pre_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
1171
+ let(:post_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
1172
+
1173
+ context "when using partition key" do
1174
+ before do
1175
+ # This call will make a blocking request to the metadata cache
1176
+ producer.produce(
1177
+ topic: "produce_test_topic",
1178
+ payload: "payload headers",
1179
+ partition_key: "test"
1180
+ ).wait
1181
+
1182
+ pre_statistics_ttl
1183
+
1184
+ # We wait to make sure that statistics are triggered and that there is a refresh
1185
+ sleep(1.5)
1186
+
1187
+ post_statistics_ttl
1188
+ end
1189
+
1190
+ it 'expect not to update ttl on the partitions count cache via statistics' do
1191
+ expect(pre_statistics_ttl).to eq post_statistics_ttl
1192
+ end
1193
+ end
1194
+
1195
+ context "when not using partition key" do
1196
+ before do
1197
+ # This call will make a blocking request to the metadata cache
1198
+ producer.produce(
1199
+ topic: "produce_test_topic",
1200
+ payload: "payload headers"
1201
+ ).wait
1202
+
1203
+ pre_statistics_ttl
1204
+
1205
+ # We wait to make sure that statistics are triggered and that there is a refresh
1206
+ sleep(1.5)
1207
+
1208
+ # This should not be populated because stats are not in use
1209
+ post_statistics_ttl
1210
+ end
1211
+
1212
+ it 'expect not to update ttl on the partitions count cache via anything' do
1213
+ expect(pre_statistics_ttl).to be_nil
1214
+ expect(post_statistics_ttl).to be_nil
1215
+ end
1216
+ end
1217
+ end
1218
+
1219
+ describe 'with other fiber closing' do
1220
+ context 'when we create many fibers and close producer in some of them' do
1221
+ it 'expect not to crash ruby' do
1222
+ 10.times do |i|
1223
+ producer = rdkafka_producer_config.producer
1224
+
1225
+ Fiber.new do
1226
+ GC.start
1227
+ producer.close
1228
+ end.resume
1229
+ end
1230
+ end
1231
+ end
1232
+ end
1233
+
1234
+ let(:producer) { rdkafka_producer_config.producer }
1235
+ let(:all_partitioners) { %w(random consistent consistent_random murmur2 murmur2_random fnv1a fnv1a_random) }
1236
+
1237
+ describe "partitioner behavior through producer API" do
1238
+ context "testing all partitioners with same key" do
1239
+ it "should not return partition 0 for all partitioners" do
1240
+ test_key = "test-key-123"
1241
+ results = {}
1242
+
1243
+ all_partitioners.each do |partitioner|
1244
+ handle = producer.produce(
1245
+ topic: "partitioner_test_topic",
1246
+ payload: "test payload",
1247
+ partition_key: test_key,
1248
+ partitioner: partitioner
1249
+ )
1250
+
1251
+ report = handle.wait(max_wait_timeout: 5)
1252
+ results[partitioner] = report.partition
1253
+ end
1254
+
1255
+ # Should not all be the same partition (especially not all 0)
1256
+ unique_partitions = results.values.uniq
1257
+ expect(unique_partitions.size).to be > 1
1258
+ end
1259
+ end
1260
+
1261
+ context "empty string partition key" do
1262
+ it "should produce message with empty partition key without crashing and go to partition 0 for all partitioners" do
1263
+ all_partitioners.each do |partitioner|
1264
+ handle = producer.produce(
1265
+ topic: "partitioner_test_topic",
1266
+ payload: "test payload",
1267
+ key: "test-key",
1268
+ partition_key: "",
1269
+ partitioner: partitioner
1270
+ )
1271
+
1272
+ report = handle.wait(max_wait_timeout: 5)
1273
+ expect(report.partition).to be >= 0
1274
+ end
1275
+ end
1276
+ end
1277
+
1278
+ context "nil partition key" do
1279
+ it "should handle nil partition key gracefully" do
1280
+ handle = producer.produce(
1281
+ topic: "partitioner_test_topic",
1282
+ payload: "test payload",
1283
+ key: "test-key",
1284
+ partition_key: nil
1285
+ )
1286
+
1287
+ report = handle.wait(max_wait_timeout: 5)
1288
+ expect(report.partition).to be >= 0
1289
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1290
+ end
1291
+ end
1292
+
1293
+ context "various key types and lengths with different partitioners" do
1294
+ it "should handle very short keys with all partitioners" do
1295
+ all_partitioners.each do |partitioner|
1296
+ handle = producer.produce(
1297
+ topic: "partitioner_test_topic",
1298
+ payload: "test payload",
1299
+ partition_key: "a",
1300
+ partitioner: partitioner
1301
+ )
1302
+
1303
+ report = handle.wait(max_wait_timeout: 5)
1304
+ expect(report.partition).to be >= 0
1305
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1306
+ end
1307
+ end
1308
+
1309
+ it "should handle very long keys with all partitioners" do
1310
+ long_key = "a" * 1000
1311
+
1312
+ all_partitioners.each do |partitioner|
1313
+ handle = producer.produce(
1314
+ topic: "partitioner_test_topic",
1315
+ payload: "test payload",
1316
+ partition_key: long_key,
1317
+ partitioner: partitioner
1318
+ )
1319
+
1320
+ report = handle.wait(max_wait_timeout: 5)
1321
+ expect(report.partition).to be >= 0
1322
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1323
+ end
1324
+ end
1325
+
1326
+ it "should handle unicode keys with all partitioners" do
1327
+ unicode_key = "测试键值🚀"
1328
+
1329
+ all_partitioners.each do |partitioner|
1330
+ handle = producer.produce(
1331
+ topic: "partitioner_test_topic",
1332
+ payload: "test payload",
1333
+ partition_key: unicode_key,
1334
+ partitioner: partitioner
1335
+ )
1336
+
1337
+ report = handle.wait(max_wait_timeout: 5)
1338
+ expect(report.partition).to be >= 0
1339
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1340
+ end
1341
+ end
1342
+ end
1343
+
1344
+ context "consistency testing for deterministic partitioners" do
1345
+ %w(consistent murmur2 fnv1a).each do |partitioner|
1346
+ it "should consistently route same partition key to same partition with #{partitioner}" do
1347
+ partition_key = "consistent-test-key"
1348
+
1349
+ # Produce multiple messages with same partition key
1350
+ reports = 5.times.map do
1351
+ handle = producer.produce(
1352
+ topic: "partitioner_test_topic",
1353
+ payload: "test payload #{Time.now.to_f}",
1354
+ partition_key: partition_key,
1355
+ partitioner: partitioner
1356
+ )
1357
+ handle.wait(max_wait_timeout: 5)
1358
+ end
1359
+
1360
+ # All should go to same partition
1361
+ partitions = reports.map(&:partition).uniq
1362
+ expect(partitions.size).to eq(1)
1363
+ end
1364
+ end
1365
+ end
1366
+
1367
+ context "randomness testing for random partitioners" do
1368
+ %w(random consistent_random murmur2_random fnv1a_random).each do |partitioner|
1369
+ it "should potentially distribute across partitions with #{partitioner}" do
1370
+ # Note: random partitioners might still return same value by chance
1371
+ partition_key = "random-test-key"
1372
+
1373
+ reports = 10.times.map do
1374
+ handle = producer.produce(
1375
+ topic: "partitioner_test_topic",
1376
+ payload: "test payload #{Time.now.to_f}",
1377
+ partition_key: partition_key,
1378
+ partitioner: partitioner
1379
+ )
1380
+ handle.wait(max_wait_timeout: 5)
1381
+ end
1382
+
1383
+ partitions = reports.map(&:partition)
1384
+
1385
+ # Just ensure they're valid partitions
1386
+ partitions.each do |partition|
1387
+ expect(partition).to be >= 0
1388
+ expect(partition).to be < producer.partition_count("partitioner_test_topic")
1389
+ end
1390
+ end
1391
+ end
1392
+ end
1393
+
1394
+ context "comparing different partitioners with same key" do
1395
+ it "should route different partition keys to potentially different partitions" do
1396
+ keys = ["key1", "key2", "key3", "key4", "key5"]
1397
+
1398
+ all_partitioners.each do |partitioner|
1399
+ reports = keys.map do |key|
1400
+ handle = producer.produce(
1401
+ topic: "partitioner_test_topic",
1402
+ payload: "test payload",
1403
+ partition_key: key,
1404
+ partitioner: partitioner
1405
+ )
1406
+ handle.wait(max_wait_timeout: 5)
1407
+ end
1408
+
1409
+ partitions = reports.map(&:partition).uniq
1410
+
1411
+ # Should distribute across multiple partitions for most partitioners
1412
+ # (though some might hash all keys to same partition by chance)
1413
+ expect(partitions.all? { |p| p >= 0 && p < producer.partition_count("partitioner_test_topic") }).to be true
1414
+ end
1415
+ end
1416
+ end
1417
+
1418
+ context "partition key vs regular key behavior" do
1419
+ it "should use partition key for partitioning when both key and partition_key are provided" do
1420
+ # Use keys that would hash to different partitions
1421
+ regular_key = "regular-key-123"
1422
+ partition_key = "partition-key-456"
1423
+
1424
+ # Message with both keys
1425
+ handle1 = producer.produce(
1426
+ topic: "partitioner_test_topic",
1427
+ payload: "test payload 1",
1428
+ key: regular_key,
1429
+ partition_key: partition_key
1430
+ )
1431
+
1432
+ # Message with only partition key (should go to same partition)
1433
+ handle2 = producer.produce(
1434
+ topic: "partitioner_test_topic",
1435
+ payload: "test payload 2",
1436
+ partition_key: partition_key
1437
+ )
1438
+
1439
+ # Message with only regular key (should go to different partition)
1440
+ handle3 = producer.produce(
1441
+ topic: "partitioner_test_topic",
1442
+ payload: "test payload 3",
1443
+ key: regular_key
1444
+ )
1445
+
1446
+ report1 = handle1.wait(max_wait_timeout: 5)
1447
+ report2 = handle2.wait(max_wait_timeout: 5)
1448
+ report3 = handle3.wait(max_wait_timeout: 5)
1449
+
1450
+ # Messages 1 and 2 should go to same partition (both use partition_key)
1451
+ expect(report1.partition).to eq(report2.partition)
1452
+
1453
+ # Message 3 should potentially go to different partition (uses regular key)
1454
+ expect(report3.partition).not_to eq(report1.partition)
1455
+ end
1456
+ end
1457
+
1458
+ context "edge case combinations with different partitioners" do
1459
+ it "should handle nil partition key with all partitioners" do
1460
+ all_partitioners.each do |partitioner|
1461
+ handle = producer.produce(
1462
+ topic: "partitioner_test_topic",
1463
+ payload: "test payload",
1464
+ key: "test-key",
1465
+ partition_key: nil,
1466
+ partitioner: partitioner
1467
+ )
1468
+
1469
+ report = handle.wait(max_wait_timeout: 5)
1470
+ expect(report.partition).to be >= 0
1471
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1472
+ end
1473
+ end
1474
+
1475
+ it "should handle whitespace-only partition key with all partitioners" do
1476
+ all_partitioners.each do |partitioner|
1477
+ handle = producer.produce(
1478
+ topic: "partitioner_test_topic",
1479
+ payload: "test payload",
1480
+ partition_key: " ",
1481
+ partitioner: partitioner
1482
+ )
1483
+
1484
+ report = handle.wait(max_wait_timeout: 5)
1485
+ expect(report.partition).to be >= 0
1486
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1487
+ end
1488
+ end
1489
+
1490
+ it "should handle newline characters in partition key with all partitioners" do
1491
+ all_partitioners.each do |partitioner|
1492
+ handle = producer.produce(
1493
+ topic: "partitioner_test_topic",
1494
+ payload: "test payload",
1495
+ partition_key: "key\nwith\nnewlines",
1496
+ partitioner: partitioner
1497
+ )
1498
+
1499
+ report = handle.wait(max_wait_timeout: 5)
1500
+ expect(report.partition).to be >= 0
1501
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1502
+ end
1503
+ end
1504
+ end
1505
+
1506
+ context "debugging partitioner issues" do
1507
+ it "should show if all partitioners return 0 (indicating a problem)" do
1508
+ test_key = "debug-test-key"
1509
+ zero_count = 0
1510
+
1511
+ all_partitioners.each do |partitioner|
1512
+ handle = producer.produce(
1513
+ topic: "partitioner_test_topic",
1514
+ payload: "debug payload",
1515
+ partition_key: test_key,
1516
+ partitioner: partitioner
1517
+ )
1518
+
1519
+ report = handle.wait(max_wait_timeout: 5)
1520
+ zero_count += 1 if report.partition == 0
1521
+ end
1522
+
1523
+ expect(zero_count).to be < all_partitioners.size
1524
+ end
1525
+ end
1526
+ end
1527
+ end