rdkafka 0.21.0 → 0.22.2

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +3 -0
  3. data/.github/workflows/ci_linux_x86_64_gnu.yml +271 -0
  4. data/.github/workflows/ci_linux_x86_64_musl.yml +194 -0
  5. data/.github/workflows/ci_macos_arm64.yml +284 -0
  6. data/.github/workflows/push_linux_x86_64_gnu.yml +65 -0
  7. data/.github/workflows/push_linux_x86_64_musl.yml +79 -0
  8. data/.github/workflows/push_macos_arm64.yml +54 -0
  9. data/.github/workflows/push_ruby.yml +37 -0
  10. data/.github/workflows/verify-action-pins.yml +16 -0
  11. data/.ruby-version +1 -1
  12. data/CHANGELOG.md +27 -0
  13. data/README.md +3 -2
  14. data/Rakefile +0 -2
  15. data/docker-compose.yml +1 -1
  16. data/ext/Rakefile +1 -1
  17. data/ext/build_common.sh +361 -0
  18. data/ext/build_linux_x86_64_gnu.sh +306 -0
  19. data/ext/build_linux_x86_64_musl.sh +763 -0
  20. data/ext/build_macos_arm64.sh +550 -0
  21. data/lib/rdkafka/bindings.rb +31 -5
  22. data/lib/rdkafka/config.rb +8 -4
  23. data/lib/rdkafka/consumer/headers.rb +14 -3
  24. data/lib/rdkafka/native_kafka.rb +8 -2
  25. data/lib/rdkafka/producer/partitions_count_cache.rb +216 -0
  26. data/lib/rdkafka/producer.rb +70 -41
  27. data/lib/rdkafka/version.rb +1 -1
  28. data/lib/rdkafka.rb +1 -0
  29. data/rdkafka.gemspec +27 -10
  30. data/renovate.json +87 -1
  31. data/spec/rdkafka/admin_spec.rb +229 -12
  32. data/spec/rdkafka/bindings_spec.rb +0 -33
  33. data/spec/rdkafka/config_spec.rb +17 -15
  34. data/spec/rdkafka/consumer/headers_spec.rb +26 -10
  35. data/spec/rdkafka/consumer_spec.rb +74 -15
  36. data/spec/rdkafka/metadata_spec.rb +2 -2
  37. data/spec/rdkafka/producer/partitions_count_cache_spec.rb +359 -0
  38. data/spec/rdkafka/producer_spec.rb +531 -8
  39. data/spec/spec_helper.rb +30 -7
  40. metadata +45 -87
  41. checksums.yaml.gz.sig +0 -0
  42. data/.github/workflows/ci.yml +0 -83
  43. data/Guardfile +0 -19
  44. data/certs/cert.pem +0 -26
  45. data.tar.gz.sig +0 -0
  46. metadata.gz.sig +0 -0
@@ -53,7 +53,7 @@ describe Rdkafka::Producer do
53
53
  let(:producer) do
54
54
  rdkafka_producer_config(
55
55
  'message.timeout.ms': 1_000_000,
56
- :"bootstrap.servers" => "localhost:9094",
56
+ :"bootstrap.servers" => "127.0.0.1:9094",
57
57
  ).producer
58
58
  end
59
59
 
@@ -340,7 +340,7 @@ describe Rdkafka::Producer do
340
340
  )
341
341
  end
342
342
 
343
- expect(messages[0].partition).to eq 0
343
+ expect(messages[0].partition).to be >= 0
344
344
  expect(messages[0].key).to eq 'a'
345
345
  end
346
346
 
@@ -364,6 +364,48 @@ describe Rdkafka::Producer do
364
364
  expect(message.key).to eq "key utf8"
365
365
  end
366
366
 
367
+ it "should produce a message to a non-existing topic with key and partition key" do
368
+ new_topic = "it-#{SecureRandom.uuid}"
369
+
370
+ handle = producer.produce(
371
+ # Needs to be a new topic each time
372
+ topic: new_topic,
373
+ payload: "payload",
374
+ key: "key",
375
+ partition_key: "partition_key",
376
+ label: "label"
377
+ )
378
+
379
+ # Should be pending at first
380
+ expect(handle.pending?).to be true
381
+ expect(handle.label).to eq "label"
382
+
383
+ # Check delivery handle and report
384
+ report = handle.wait(max_wait_timeout: 5)
385
+ expect(handle.pending?).to be false
386
+ expect(report).not_to be_nil
387
+ expect(report.partition).to eq 0
388
+ expect(report.offset).to be >= 0
389
+ expect(report.label).to eq "label"
390
+
391
+ # Flush and close producer
392
+ producer.flush
393
+ producer.close
394
+
395
+ # Consume message and verify its content
396
+ message = wait_for_message(
397
+ topic: new_topic,
398
+ delivery_report: report,
399
+ consumer: consumer
400
+ )
401
+ expect(message.partition).to eq 0
402
+ expect(message.payload).to eq "payload"
403
+ expect(message.key).to eq "key"
404
+ # Since api.version.request is on by default we will get
405
+ # the message creation timestamp if it's not set.
406
+ expect(message.timestamp).to be_within(10).of(Time.now)
407
+ end
408
+
367
409
  context "timestamp" do
368
410
  it "should raise a type error if not nil, integer or time" do
369
411
  expect {
@@ -623,7 +665,7 @@ describe Rdkafka::Producer do
623
665
  context "when not being able to deliver the message" do
624
666
  let(:producer) do
625
667
  rdkafka_producer_config(
626
- "bootstrap.servers": "localhost:9093",
668
+ "bootstrap.servers": "127.0.0.1:9093",
627
669
  "message.timeout.ms": 100
628
670
  ).producer
629
671
  end
@@ -637,6 +679,25 @@ describe Rdkafka::Producer do
637
679
  end
638
680
  end
639
681
 
682
+ context "when topic does not exist and allow.auto.create.topics is false" do
683
+ let(:producer) do
684
+ rdkafka_producer_config(
685
+ "bootstrap.servers": "127.0.0.1:9092",
686
+ "message.timeout.ms": 100,
687
+ "allow.auto.create.topics": false
688
+ ).producer
689
+ end
690
+
691
+ it "should contain the error in the response when not deliverable" do
692
+ handler = producer.produce(topic: "it-#{SecureRandom.uuid}", payload: nil, label: 'na')
693
+ # Wait for the async callbacks and delivery registry to update
694
+ sleep(2)
695
+ expect(handler.create_result.error).to be_a(Rdkafka::RdkafkaError)
696
+ expect(handler.create_result.error.code).to eq(:msg_timed_out)
697
+ expect(handler.create_result.label).to eq('na')
698
+ end
699
+ end
700
+
640
701
  describe '#partition_count' do
641
702
  it { expect(producer.partition_count('consume_test_topic')).to eq(3) }
642
703
 
@@ -654,12 +715,11 @@ describe Rdkafka::Producer do
654
715
 
655
716
  context 'when the partition count value was cached but time expired' do
656
717
  before do
657
- allow(::Process).to receive(:clock_gettime).and_return(0, 30.02)
658
- producer.partition_count('consume_test_topic')
718
+ ::Rdkafka::Producer.partitions_count_cache = Rdkafka::Producer::PartitionsCountCache.new
659
719
  allow(::Rdkafka::Metadata).to receive(:new).and_call_original
660
720
  end
661
721
 
662
- it 'expect not to query it again' do
722
+ it 'expect to query it again' do
663
723
  producer.partition_count('consume_test_topic')
664
724
  expect(::Rdkafka::Metadata).to have_received(:new)
665
725
  end
@@ -694,7 +754,7 @@ describe Rdkafka::Producer do
694
754
  context 'when it cannot flush due to a timeout' do
695
755
  let(:producer) do
696
756
  rdkafka_producer_config(
697
- "bootstrap.servers": "localhost:9093",
757
+ "bootstrap.servers": "127.0.0.1:9093",
698
758
  "message.timeout.ms": 2_000
699
759
  ).producer
700
760
  end
@@ -741,7 +801,7 @@ describe Rdkafka::Producer do
741
801
  context 'when there are outgoing things in the queue' do
742
802
  let(:producer) do
743
803
  rdkafka_producer_config(
744
- "bootstrap.servers": "localhost:9093",
804
+ "bootstrap.servers": "127.0.0.1:9093",
745
805
  "message.timeout.ms": 2_000
746
806
  ).producer
747
807
  end
@@ -819,4 +879,467 @@ describe Rdkafka::Producer do
819
879
  end
820
880
  end
821
881
  end
882
+
883
+ describe "#produce with headers" do
884
+ it "should produce a message with array headers" do
885
+ headers = {
886
+ "version" => ["2.1.3", "2.1.4"],
887
+ "type" => "String"
888
+ }
889
+
890
+ report = producer.produce(
891
+ topic: "consume_test_topic",
892
+ key: "key headers",
893
+ headers: headers
894
+ ).wait
895
+
896
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
897
+ expect(message).to be
898
+ expect(message.key).to eq('key headers')
899
+ expect(message.headers['type']).to eq('String')
900
+ expect(message.headers['version']).to eq(["2.1.3", "2.1.4"])
901
+ end
902
+
903
+ it "should produce a message with single value headers" do
904
+ headers = {
905
+ "version" => "2.1.3",
906
+ "type" => "String"
907
+ }
908
+
909
+ report = producer.produce(
910
+ topic: "consume_test_topic",
911
+ key: "key headers",
912
+ headers: headers
913
+ ).wait
914
+
915
+ message = wait_for_message(topic: "consume_test_topic", consumer: consumer, delivery_report: report)
916
+ expect(message).to be
917
+ expect(message.key).to eq('key headers')
918
+ expect(message.headers['type']).to eq('String')
919
+ expect(message.headers['version']).to eq('2.1.3')
920
+ end
921
+ end
922
+
923
+ describe 'with active statistics callback' do
924
+ let(:producer) do
925
+ rdkafka_producer_config('statistics.interval.ms': 1_000).producer
926
+ end
927
+
928
+ let(:count_cache_hash) { described_class.partitions_count_cache.to_h }
929
+ let(:pre_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
930
+ let(:post_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
931
+
932
+ context "when using partition key" do
933
+ before do
934
+ Rdkafka::Config.statistics_callback = ->(*) {}
935
+
936
+ # This call will make a blocking request to the metadata cache
937
+ producer.produce(
938
+ topic: "produce_test_topic",
939
+ payload: "payload headers",
940
+ partition_key: "test"
941
+ ).wait
942
+
943
+ pre_statistics_ttl
944
+
945
+ # We wait to make sure that statistics are triggered and that there is a refresh
946
+ sleep(1.5)
947
+
948
+ post_statistics_ttl
949
+ end
950
+
951
+ it 'expect to update ttl on the partitions count cache via statistics' do
952
+ expect(pre_statistics_ttl).to be < post_statistics_ttl
953
+ end
954
+ end
955
+
956
+ context "when not using partition key" do
957
+ before do
958
+ Rdkafka::Config.statistics_callback = ->(*) {}
959
+
960
+ # This call will make a blocking request to the metadata cache
961
+ producer.produce(
962
+ topic: "produce_test_topic",
963
+ payload: "payload headers"
964
+ ).wait
965
+
966
+ pre_statistics_ttl
967
+
968
+ # We wait to make sure that statistics are triggered and that there is a refresh
969
+ sleep(1.5)
970
+
971
+ # This will anyhow be populated from statistic
972
+ post_statistics_ttl
973
+ end
974
+
975
+ it 'expect not to update ttl on the partitions count cache via blocking but via use stats' do
976
+ expect(pre_statistics_ttl).to be_nil
977
+ expect(post_statistics_ttl).not_to be_nil
978
+ end
979
+ end
980
+ end
981
+
982
+ describe 'without active statistics callback' do
983
+ let(:producer) do
984
+ rdkafka_producer_config('statistics.interval.ms': 1_000).producer
985
+ end
986
+
987
+ let(:count_cache_hash) { described_class.partitions_count_cache.to_h }
988
+ let(:pre_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
989
+ let(:post_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
990
+
991
+ context "when using partition key" do
992
+ before do
993
+ # This call will make a blocking request to the metadata cache
994
+ producer.produce(
995
+ topic: "produce_test_topic",
996
+ payload: "payload headers",
997
+ partition_key: "test"
998
+ ).wait
999
+
1000
+ pre_statistics_ttl
1001
+
1002
+ # We wait to make sure that statistics are triggered and that there is a refresh
1003
+ sleep(1.5)
1004
+
1005
+ post_statistics_ttl
1006
+ end
1007
+
1008
+ it 'expect not to update ttl on the partitions count cache via statistics' do
1009
+ expect(pre_statistics_ttl).to eq post_statistics_ttl
1010
+ end
1011
+ end
1012
+
1013
+ context "when not using partition key" do
1014
+ before do
1015
+ # This call will make a blocking request to the metadata cache
1016
+ producer.produce(
1017
+ topic: "produce_test_topic",
1018
+ payload: "payload headers"
1019
+ ).wait
1020
+
1021
+ pre_statistics_ttl
1022
+
1023
+ # We wait to make sure that statistics are triggered and that there is a refresh
1024
+ sleep(1.5)
1025
+
1026
+ # This should not be populated because stats are not in use
1027
+ post_statistics_ttl
1028
+ end
1029
+
1030
+ it 'expect not to update ttl on the partitions count cache via anything' do
1031
+ expect(pre_statistics_ttl).to be_nil
1032
+ expect(post_statistics_ttl).to be_nil
1033
+ end
1034
+ end
1035
+ end
1036
+
1037
+ describe 'with other fiber closing' do
1038
+ context 'when we create many fibers and close producer in some of them' do
1039
+ it 'expect not to crash ruby' do
1040
+ 10.times do |i|
1041
+ producer = rdkafka_producer_config.producer
1042
+
1043
+ Fiber.new do
1044
+ GC.start
1045
+ producer.close
1046
+ end.resume
1047
+ end
1048
+ end
1049
+ end
1050
+ end
1051
+
1052
+ let(:producer) { rdkafka_producer_config.producer }
1053
+ let(:all_partitioners) { %w(random consistent consistent_random murmur2 murmur2_random fnv1a fnv1a_random) }
1054
+
1055
+ describe "partitioner behavior through producer API" do
1056
+ context "testing all partitioners with same key" do
1057
+ it "should not return partition 0 for all partitioners" do
1058
+ test_key = "test-key-123"
1059
+ results = {}
1060
+
1061
+ all_partitioners.each do |partitioner|
1062
+ handle = producer.produce(
1063
+ topic: "partitioner_test_topic",
1064
+ payload: "test payload",
1065
+ partition_key: test_key,
1066
+ partitioner: partitioner
1067
+ )
1068
+
1069
+ report = handle.wait(max_wait_timeout: 5)
1070
+ results[partitioner] = report.partition
1071
+ end
1072
+
1073
+ # Should not all be the same partition (especially not all 0)
1074
+ unique_partitions = results.values.uniq
1075
+ expect(unique_partitions.size).to be > 1
1076
+ end
1077
+ end
1078
+
1079
+ context "empty string partition key" do
1080
+ it "should produce message with empty partition key without crashing and go to partition 0 for all partitioners" do
1081
+ all_partitioners.each do |partitioner|
1082
+ handle = producer.produce(
1083
+ topic: "partitioner_test_topic",
1084
+ payload: "test payload",
1085
+ key: "test-key",
1086
+ partition_key: "",
1087
+ partitioner: partitioner
1088
+ )
1089
+
1090
+ report = handle.wait(max_wait_timeout: 5)
1091
+ expect(report.partition).to be >= 0
1092
+ end
1093
+ end
1094
+ end
1095
+
1096
+ context "nil partition key" do
1097
+ it "should handle nil partition key gracefully" do
1098
+ handle = producer.produce(
1099
+ topic: "partitioner_test_topic",
1100
+ payload: "test payload",
1101
+ key: "test-key",
1102
+ partition_key: nil
1103
+ )
1104
+
1105
+ report = handle.wait(max_wait_timeout: 5)
1106
+ expect(report.partition).to be >= 0
1107
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1108
+ end
1109
+ end
1110
+
1111
+ context "various key types and lengths with different partitioners" do
1112
+ it "should handle very short keys with all partitioners" do
1113
+ all_partitioners.each do |partitioner|
1114
+ handle = producer.produce(
1115
+ topic: "partitioner_test_topic",
1116
+ payload: "test payload",
1117
+ partition_key: "a",
1118
+ partitioner: partitioner
1119
+ )
1120
+
1121
+ report = handle.wait(max_wait_timeout: 5)
1122
+ expect(report.partition).to be >= 0
1123
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1124
+ end
1125
+ end
1126
+
1127
+ it "should handle very long keys with all partitioners" do
1128
+ long_key = "a" * 1000
1129
+
1130
+ all_partitioners.each do |partitioner|
1131
+ handle = producer.produce(
1132
+ topic: "partitioner_test_topic",
1133
+ payload: "test payload",
1134
+ partition_key: long_key,
1135
+ partitioner: partitioner
1136
+ )
1137
+
1138
+ report = handle.wait(max_wait_timeout: 5)
1139
+ expect(report.partition).to be >= 0
1140
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1141
+ end
1142
+ end
1143
+
1144
+ it "should handle unicode keys with all partitioners" do
1145
+ unicode_key = "测试键值🚀"
1146
+
1147
+ all_partitioners.each do |partitioner|
1148
+ handle = producer.produce(
1149
+ topic: "partitioner_test_topic",
1150
+ payload: "test payload",
1151
+ partition_key: unicode_key,
1152
+ partitioner: partitioner
1153
+ )
1154
+
1155
+ report = handle.wait(max_wait_timeout: 5)
1156
+ expect(report.partition).to be >= 0
1157
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1158
+ end
1159
+ end
1160
+ end
1161
+
1162
+ context "consistency testing for deterministic partitioners" do
1163
+ %w(consistent murmur2 fnv1a).each do |partitioner|
1164
+ it "should consistently route same partition key to same partition with #{partitioner}" do
1165
+ partition_key = "consistent-test-key"
1166
+
1167
+ # Produce multiple messages with same partition key
1168
+ reports = 5.times.map do
1169
+ handle = producer.produce(
1170
+ topic: "partitioner_test_topic",
1171
+ payload: "test payload #{Time.now.to_f}",
1172
+ partition_key: partition_key,
1173
+ partitioner: partitioner
1174
+ )
1175
+ handle.wait(max_wait_timeout: 5)
1176
+ end
1177
+
1178
+ # All should go to same partition
1179
+ partitions = reports.map(&:partition).uniq
1180
+ expect(partitions.size).to eq(1)
1181
+ end
1182
+ end
1183
+ end
1184
+
1185
+ context "randomness testing for random partitioners" do
1186
+ %w(random consistent_random murmur2_random fnv1a_random).each do |partitioner|
1187
+ it "should potentially distribute across partitions with #{partitioner}" do
1188
+ # Note: random partitioners might still return same value by chance
1189
+ partition_key = "random-test-key"
1190
+
1191
+ reports = 10.times.map do
1192
+ handle = producer.produce(
1193
+ topic: "partitioner_test_topic",
1194
+ payload: "test payload #{Time.now.to_f}",
1195
+ partition_key: partition_key,
1196
+ partitioner: partitioner
1197
+ )
1198
+ handle.wait(max_wait_timeout: 5)
1199
+ end
1200
+
1201
+ partitions = reports.map(&:partition)
1202
+
1203
+ # Just ensure they're valid partitions
1204
+ partitions.each do |partition|
1205
+ expect(partition).to be >= 0
1206
+ expect(partition).to be < producer.partition_count("partitioner_test_topic")
1207
+ end
1208
+ end
1209
+ end
1210
+ end
1211
+
1212
+ context "comparing different partitioners with same key" do
1213
+ it "should route different partition keys to potentially different partitions" do
1214
+ keys = ["key1", "key2", "key3", "key4", "key5"]
1215
+
1216
+ all_partitioners.each do |partitioner|
1217
+ reports = keys.map do |key|
1218
+ handle = producer.produce(
1219
+ topic: "partitioner_test_topic",
1220
+ payload: "test payload",
1221
+ partition_key: key,
1222
+ partitioner: partitioner
1223
+ )
1224
+ handle.wait(max_wait_timeout: 5)
1225
+ end
1226
+
1227
+ partitions = reports.map(&:partition).uniq
1228
+
1229
+ # Should distribute across multiple partitions for most partitioners
1230
+ # (though some might hash all keys to same partition by chance)
1231
+ expect(partitions.all? { |p| p >= 0 && p < producer.partition_count("partitioner_test_topic") }).to be true
1232
+ end
1233
+ end
1234
+ end
1235
+
1236
+ context "partition key vs regular key behavior" do
1237
+ it "should use partition key for partitioning when both key and partition_key are provided" do
1238
+ # Use keys that would hash to different partitions
1239
+ regular_key = "regular-key-123"
1240
+ partition_key = "partition-key-456"
1241
+
1242
+ # Message with both keys
1243
+ handle1 = producer.produce(
1244
+ topic: "partitioner_test_topic",
1245
+ payload: "test payload 1",
1246
+ key: regular_key,
1247
+ partition_key: partition_key
1248
+ )
1249
+
1250
+ # Message with only partition key (should go to same partition)
1251
+ handle2 = producer.produce(
1252
+ topic: "partitioner_test_topic",
1253
+ payload: "test payload 2",
1254
+ partition_key: partition_key
1255
+ )
1256
+
1257
+ # Message with only regular key (should go to different partition)
1258
+ handle3 = producer.produce(
1259
+ topic: "partitioner_test_topic",
1260
+ payload: "test payload 3",
1261
+ key: regular_key
1262
+ )
1263
+
1264
+ report1 = handle1.wait(max_wait_timeout: 5)
1265
+ report2 = handle2.wait(max_wait_timeout: 5)
1266
+ report3 = handle3.wait(max_wait_timeout: 5)
1267
+
1268
+ # Messages 1 and 2 should go to same partition (both use partition_key)
1269
+ expect(report1.partition).to eq(report2.partition)
1270
+
1271
+ # Message 3 should potentially go to different partition (uses regular key)
1272
+ expect(report3.partition).not_to eq(report1.partition)
1273
+ end
1274
+ end
1275
+
1276
+ context "edge case combinations with different partitioners" do
1277
+ it "should handle nil partition key with all partitioners" do
1278
+ all_partitioners.each do |partitioner|
1279
+ handle = producer.produce(
1280
+ topic: "partitioner_test_topic",
1281
+ payload: "test payload",
1282
+ key: "test-key",
1283
+ partition_key: nil,
1284
+ partitioner: partitioner
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
+ it "should handle whitespace-only partition key with all partitioners" do
1294
+ all_partitioners.each do |partitioner|
1295
+ handle = producer.produce(
1296
+ topic: "partitioner_test_topic",
1297
+ payload: "test payload",
1298
+ partition_key: " ",
1299
+ partitioner: partitioner
1300
+ )
1301
+
1302
+ report = handle.wait(max_wait_timeout: 5)
1303
+ expect(report.partition).to be >= 0
1304
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1305
+ end
1306
+ end
1307
+
1308
+ it "should handle newline characters in partition key with all partitioners" do
1309
+ all_partitioners.each do |partitioner|
1310
+ handle = producer.produce(
1311
+ topic: "partitioner_test_topic",
1312
+ payload: "test payload",
1313
+ partition_key: "key\nwith\nnewlines",
1314
+ partitioner: partitioner
1315
+ )
1316
+
1317
+ report = handle.wait(max_wait_timeout: 5)
1318
+ expect(report.partition).to be >= 0
1319
+ expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
1320
+ end
1321
+ end
1322
+ end
1323
+
1324
+ context "debugging partitioner issues" do
1325
+ it "should show if all partitioners return 0 (indicating a problem)" do
1326
+ test_key = "debug-test-key"
1327
+ zero_count = 0
1328
+
1329
+ all_partitioners.each do |partitioner|
1330
+ handle = producer.produce(
1331
+ topic: "partitioner_test_topic",
1332
+ payload: "debug payload",
1333
+ partition_key: test_key,
1334
+ partitioner: partitioner
1335
+ )
1336
+
1337
+ report = handle.wait(max_wait_timeout: 5)
1338
+ zero_count += 1 if report.partition == 0
1339
+ end
1340
+
1341
+ expect(zero_count).to be < all_partitioners.size
1342
+ end
1343
+ end
1344
+ end
822
1345
  end
data/spec/spec_helper.rb CHANGED
@@ -17,7 +17,10 @@ def rdkafka_base_config
17
17
  {
18
18
  :"api.version.request" => false,
19
19
  :"broker.version.fallback" => "1.0",
20
- :"bootstrap.servers" => "localhost:9092",
20
+ :"bootstrap.servers" => "127.0.0.1:9092",
21
+ # Display statistics and refresh often just to cover those in specs
22
+ :'statistics.interval.ms' => 1_000,
23
+ :'topic.metadata.refresh.interval.ms' => 1_000
21
24
  }
22
25
  end
23
26
 
@@ -75,18 +78,32 @@ end
75
78
 
76
79
  def wait_for_message(topic:, delivery_report:, timeout_in_seconds: 30, consumer: nil)
77
80
  new_consumer = consumer.nil?
78
- consumer ||= rdkafka_consumer_config.consumer
81
+ consumer ||= rdkafka_consumer_config('allow.auto.create.topics': true).consumer
79
82
  consumer.subscribe(topic)
80
83
  timeout = Time.now.to_i + timeout_in_seconds
84
+ retry_count = 0
85
+ max_retries = 10
86
+
81
87
  loop do
82
88
  if timeout <= Time.now.to_i
83
89
  raise "Timeout of #{timeout_in_seconds} seconds reached in wait_for_message"
84
90
  end
85
- message = consumer.poll(100)
86
- if message &&
87
- message.partition == delivery_report.partition &&
88
- message.offset == delivery_report.offset
89
- return message
91
+
92
+ begin
93
+ message = consumer.poll(100)
94
+ if message &&
95
+ message.partition == delivery_report.partition &&
96
+ message.offset == delivery_report.offset
97
+ return message
98
+ end
99
+ rescue Rdkafka::RdkafkaError => e
100
+ if e.code == :unknown_topic_or_part && retry_count < max_retries
101
+ retry_count += 1
102
+ sleep(0.1) # Small delay before retry
103
+ next
104
+ else
105
+ raise
106
+ end
90
107
  end
91
108
  end
92
109
  ensure
@@ -125,6 +142,12 @@ RSpec.configure do |config|
125
142
  config.filter_run focus: true
126
143
  config.run_all_when_everything_filtered = true
127
144
 
145
+ config.before(:each) do
146
+ Rdkafka::Config.statistics_callback = nil
147
+ # We need to clear it so state does not leak between specs
148
+ Rdkafka::Producer.partitions_count_cache.to_h.clear
149
+ end
150
+
128
151
  config.before(:suite) do
129
152
  admin = rdkafka_config.admin
130
153
  {