karafka-rdkafka 0.20.0.rc2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci_linux_x86_64_gnu.yml +249 -0
- data/.github/workflows/ci_linux_x86_64_musl.yml +205 -0
- data/.github/workflows/ci_macos_arm64.yml +306 -0
- data/.github/workflows/push_linux_x86_64_gnu.yml +64 -0
- data/.github/workflows/push_linux_x86_64_musl.yml +77 -0
- data/.github/workflows/push_macos_arm64.yml +54 -0
- data/.github/workflows/push_ruby.yml +37 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +25 -4
- data/README.md +2 -3
- data/Rakefile +0 -2
- data/dist/{librdkafka-2.10.0.tar.gz → librdkafka-2.8.0.tar.gz} +0 -0
- data/docker-compose.yml +1 -1
- data/ext/Rakefile +1 -1
- data/ext/build_common.sh +361 -0
- data/ext/build_linux_x86_64_gnu.sh +306 -0
- data/ext/build_linux_x86_64_musl.sh +763 -0
- data/ext/build_macos_arm64.sh +550 -0
- data/karafka-rdkafka.gemspec +51 -10
- data/lib/rdkafka/bindings.rb +32 -6
- data/lib/rdkafka/config.rb +4 -1
- data/lib/rdkafka/error.rb +8 -1
- data/lib/rdkafka/native_kafka.rb +4 -0
- data/lib/rdkafka/producer/partitions_count_cache.rb +216 -0
- data/lib/rdkafka/producer.rb +51 -34
- data/lib/rdkafka/version.rb +3 -3
- data/lib/rdkafka.rb +1 -0
- data/renovate.json +74 -0
- data/spec/rdkafka/admin_spec.rb +217 -3
- data/spec/rdkafka/bindings_spec.rb +0 -25
- data/spec/rdkafka/config_spec.rb +1 -1
- data/spec/rdkafka/consumer_spec.rb +35 -17
- data/spec/rdkafka/metadata_spec.rb +2 -2
- data/spec/rdkafka/producer/partitions_count_cache_spec.rb +359 -0
- data/spec/rdkafka/producer_spec.rb +493 -8
- data/spec/spec_helper.rb +32 -7
- metadata +37 -95
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +0 -99
- data/Guardfile +0 -19
- data/certs/cert.pem +0 -26
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -3
@@ -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" => "
|
56
|
+
:"bootstrap.servers" => "127.0.0.1:9094",
|
57
57
|
).producer
|
58
58
|
end
|
59
59
|
|
@@ -263,6 +263,8 @@ describe Rdkafka::Producer do
|
|
263
263
|
expect(message.partition).to eq 1
|
264
264
|
expect(message.payload).to eq "payload"
|
265
265
|
expect(message.key).to eq "key"
|
266
|
+
# Since api.version.request is on by default we will get
|
267
|
+
# the message creation timestamp if it's not set.
|
266
268
|
expect(message.timestamp).to be_within(10).of(Time.now)
|
267
269
|
end
|
268
270
|
|
@@ -338,7 +340,7 @@ describe Rdkafka::Producer do
|
|
338
340
|
)
|
339
341
|
end
|
340
342
|
|
341
|
-
expect(messages[0].partition).to
|
343
|
+
expect(messages[0].partition).to be >= 0
|
342
344
|
expect(messages[0].key).to eq 'a'
|
343
345
|
end
|
344
346
|
|
@@ -362,6 +364,48 @@ describe Rdkafka::Producer do
|
|
362
364
|
expect(message.key).to eq "key utf8"
|
363
365
|
end
|
364
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
|
+
|
365
409
|
context "timestamp" do
|
366
410
|
it "should raise a type error if not nil, integer or time" do
|
367
411
|
expect {
|
@@ -621,7 +665,7 @@ describe Rdkafka::Producer do
|
|
621
665
|
context "when not being able to deliver the message" do
|
622
666
|
let(:producer) do
|
623
667
|
rdkafka_producer_config(
|
624
|
-
"bootstrap.servers": "
|
668
|
+
"bootstrap.servers": "127.0.0.1:9093",
|
625
669
|
"message.timeout.ms": 100
|
626
670
|
).producer
|
627
671
|
end
|
@@ -635,6 +679,25 @@ describe Rdkafka::Producer do
|
|
635
679
|
end
|
636
680
|
end
|
637
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
|
+
|
638
701
|
describe '#partition_count' do
|
639
702
|
it { expect(producer.partition_count('example_topic')).to eq(1) }
|
640
703
|
|
@@ -652,12 +715,11 @@ describe Rdkafka::Producer do
|
|
652
715
|
|
653
716
|
context 'when the partition count value was cached but time expired' do
|
654
717
|
before do
|
655
|
-
|
656
|
-
producer.partition_count('example_topic')
|
718
|
+
::Rdkafka::Producer.partitions_count_cache = Rdkafka::Producer::PartitionsCountCache.new
|
657
719
|
allow(::Rdkafka::Metadata).to receive(:new).and_call_original
|
658
720
|
end
|
659
721
|
|
660
|
-
it 'expect
|
722
|
+
it 'expect to query it again' do
|
661
723
|
producer.partition_count('example_topic')
|
662
724
|
expect(::Rdkafka::Metadata).to have_received(:new)
|
663
725
|
end
|
@@ -719,7 +781,7 @@ describe Rdkafka::Producer do
|
|
719
781
|
context 'when it cannot flush due to a timeout' do
|
720
782
|
let(:producer) do
|
721
783
|
rdkafka_producer_config(
|
722
|
-
"bootstrap.servers": "
|
784
|
+
"bootstrap.servers": "127.0.0.1:9093",
|
723
785
|
"message.timeout.ms": 2_000
|
724
786
|
).producer
|
725
787
|
end
|
@@ -766,7 +828,7 @@ describe Rdkafka::Producer do
|
|
766
828
|
context 'when there are outgoing things in the queue' do
|
767
829
|
let(:producer) do
|
768
830
|
rdkafka_producer_config(
|
769
|
-
"bootstrap.servers": "
|
831
|
+
"bootstrap.servers": "127.0.0.1:9093",
|
770
832
|
"message.timeout.ms": 2_000
|
771
833
|
).producer
|
772
834
|
end
|
@@ -1040,4 +1102,427 @@ describe Rdkafka::Producer do
|
|
1040
1102
|
expect(message.headers['version']).to eq('2.1.3')
|
1041
1103
|
end
|
1042
1104
|
end
|
1105
|
+
|
1106
|
+
describe 'with active statistics callback' do
|
1107
|
+
let(:producer) do
|
1108
|
+
rdkafka_producer_config('statistics.interval.ms': 1_000).producer
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
let(:count_cache_hash) { described_class.partitions_count_cache.to_h }
|
1112
|
+
let(:pre_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
|
1113
|
+
let(:post_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
|
1114
|
+
|
1115
|
+
context "when using partition key" do
|
1116
|
+
before do
|
1117
|
+
Rdkafka::Config.statistics_callback = ->(*) {}
|
1118
|
+
|
1119
|
+
# This call will make a blocking request to the metadata cache
|
1120
|
+
producer.produce(
|
1121
|
+
topic: "produce_test_topic",
|
1122
|
+
payload: "payload headers",
|
1123
|
+
partition_key: "test"
|
1124
|
+
).wait
|
1125
|
+
|
1126
|
+
pre_statistics_ttl
|
1127
|
+
|
1128
|
+
# We wait to make sure that statistics are triggered and that there is a refresh
|
1129
|
+
sleep(1.5)
|
1130
|
+
|
1131
|
+
post_statistics_ttl
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
it 'expect to update ttl on the partitions count cache via statistics' do
|
1135
|
+
expect(pre_statistics_ttl).to be < post_statistics_ttl
|
1136
|
+
end
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
context "when not using partition key" do
|
1140
|
+
before do
|
1141
|
+
Rdkafka::Config.statistics_callback = ->(*) {}
|
1142
|
+
|
1143
|
+
# This call will make a blocking request to the metadata cache
|
1144
|
+
producer.produce(
|
1145
|
+
topic: "produce_test_topic",
|
1146
|
+
payload: "payload headers"
|
1147
|
+
).wait
|
1148
|
+
|
1149
|
+
pre_statistics_ttl
|
1150
|
+
|
1151
|
+
# We wait to make sure that statistics are triggered and that there is a refresh
|
1152
|
+
sleep(1.5)
|
1153
|
+
|
1154
|
+
# This will anyhow be populated from statistic
|
1155
|
+
post_statistics_ttl
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
it 'expect not to update ttl on the partitions count cache via blocking but via use stats' do
|
1159
|
+
expect(pre_statistics_ttl).to be_nil
|
1160
|
+
expect(post_statistics_ttl).not_to be_nil
|
1161
|
+
end
|
1162
|
+
end
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
describe 'without active statistics callback' do
|
1166
|
+
let(:producer) do
|
1167
|
+
rdkafka_producer_config('statistics.interval.ms': 1_000).producer
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
let(:count_cache_hash) { described_class.partitions_count_cache.to_h }
|
1171
|
+
let(:pre_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
|
1172
|
+
let(:post_statistics_ttl) { count_cache_hash.fetch('produce_test_topic', [])[0] }
|
1173
|
+
|
1174
|
+
context "when using partition key" do
|
1175
|
+
before do
|
1176
|
+
# This call will make a blocking request to the metadata cache
|
1177
|
+
producer.produce(
|
1178
|
+
topic: "produce_test_topic",
|
1179
|
+
payload: "payload headers",
|
1180
|
+
partition_key: "test"
|
1181
|
+
).wait
|
1182
|
+
|
1183
|
+
pre_statistics_ttl
|
1184
|
+
|
1185
|
+
# We wait to make sure that statistics are triggered and that there is a refresh
|
1186
|
+
sleep(1.5)
|
1187
|
+
|
1188
|
+
post_statistics_ttl
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
it 'expect not to update ttl on the partitions count cache via statistics' do
|
1192
|
+
expect(pre_statistics_ttl).to eq post_statistics_ttl
|
1193
|
+
end
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
context "when not using partition key" do
|
1197
|
+
before do
|
1198
|
+
# This call will make a blocking request to the metadata cache
|
1199
|
+
producer.produce(
|
1200
|
+
topic: "produce_test_topic",
|
1201
|
+
payload: "payload headers"
|
1202
|
+
).wait
|
1203
|
+
|
1204
|
+
pre_statistics_ttl
|
1205
|
+
|
1206
|
+
# We wait to make sure that statistics are triggered and that there is a refresh
|
1207
|
+
sleep(1.5)
|
1208
|
+
|
1209
|
+
# This should not be populated because stats are not in use
|
1210
|
+
post_statistics_ttl
|
1211
|
+
end
|
1212
|
+
|
1213
|
+
it 'expect not to update ttl on the partitions count cache via anything' do
|
1214
|
+
expect(pre_statistics_ttl).to be_nil
|
1215
|
+
expect(post_statistics_ttl).to be_nil
|
1216
|
+
end
|
1217
|
+
end
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
describe 'with other fiber closing' do
|
1221
|
+
context 'when we create many fibers and close producer in some of them' do
|
1222
|
+
it 'expect not to crash ruby' do
|
1223
|
+
10.times do |i|
|
1224
|
+
producer = rdkafka_producer_config.producer
|
1225
|
+
|
1226
|
+
Fiber.new do
|
1227
|
+
GC.start
|
1228
|
+
producer.close
|
1229
|
+
end.resume
|
1230
|
+
end
|
1231
|
+
end
|
1232
|
+
end
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
let(:producer) { rdkafka_producer_config.producer }
|
1236
|
+
let(:all_partitioners) { %w(random consistent consistent_random murmur2 murmur2_random fnv1a fnv1a_random) }
|
1237
|
+
|
1238
|
+
describe "partitioner behavior through producer API" do
|
1239
|
+
context "testing all partitioners with same key" do
|
1240
|
+
it "should not return partition 0 for all partitioners" do
|
1241
|
+
test_key = "test-key-123"
|
1242
|
+
results = {}
|
1243
|
+
|
1244
|
+
all_partitioners.each do |partitioner|
|
1245
|
+
handle = producer.produce(
|
1246
|
+
topic: "partitioner_test_topic",
|
1247
|
+
payload: "test payload",
|
1248
|
+
partition_key: test_key,
|
1249
|
+
partitioner: partitioner
|
1250
|
+
)
|
1251
|
+
|
1252
|
+
report = handle.wait(max_wait_timeout: 5)
|
1253
|
+
results[partitioner] = report.partition
|
1254
|
+
end
|
1255
|
+
|
1256
|
+
# Should not all be the same partition (especially not all 0)
|
1257
|
+
unique_partitions = results.values.uniq
|
1258
|
+
expect(unique_partitions.size).to be > 1
|
1259
|
+
end
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
context "empty string partition key" do
|
1263
|
+
it "should produce message with empty partition key without crashing and go to partition 0 for all partitioners" do
|
1264
|
+
all_partitioners.each do |partitioner|
|
1265
|
+
handle = producer.produce(
|
1266
|
+
topic: "partitioner_test_topic",
|
1267
|
+
payload: "test payload",
|
1268
|
+
key: "test-key",
|
1269
|
+
partition_key: "",
|
1270
|
+
partitioner: partitioner
|
1271
|
+
)
|
1272
|
+
|
1273
|
+
report = handle.wait(max_wait_timeout: 5)
|
1274
|
+
expect(report.partition).to be >= 0
|
1275
|
+
end
|
1276
|
+
end
|
1277
|
+
end
|
1278
|
+
|
1279
|
+
context "nil partition key" do
|
1280
|
+
it "should handle nil partition key gracefully" do
|
1281
|
+
handle = producer.produce(
|
1282
|
+
topic: "partitioner_test_topic",
|
1283
|
+
payload: "test payload",
|
1284
|
+
key: "test-key",
|
1285
|
+
partition_key: nil
|
1286
|
+
)
|
1287
|
+
|
1288
|
+
report = handle.wait(max_wait_timeout: 5)
|
1289
|
+
expect(report.partition).to be >= 0
|
1290
|
+
expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
|
1291
|
+
end
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
context "various key types and lengths with different partitioners" do
|
1295
|
+
it "should handle very short keys with all partitioners" do
|
1296
|
+
all_partitioners.each do |partitioner|
|
1297
|
+
handle = producer.produce(
|
1298
|
+
topic: "partitioner_test_topic",
|
1299
|
+
payload: "test payload",
|
1300
|
+
partition_key: "a",
|
1301
|
+
partitioner: partitioner
|
1302
|
+
)
|
1303
|
+
|
1304
|
+
report = handle.wait(max_wait_timeout: 5)
|
1305
|
+
expect(report.partition).to be >= 0
|
1306
|
+
expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
|
1307
|
+
end
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
it "should handle very long keys with all partitioners" do
|
1311
|
+
long_key = "a" * 1000
|
1312
|
+
|
1313
|
+
all_partitioners.each do |partitioner|
|
1314
|
+
handle = producer.produce(
|
1315
|
+
topic: "partitioner_test_topic",
|
1316
|
+
payload: "test payload",
|
1317
|
+
partition_key: long_key,
|
1318
|
+
partitioner: partitioner
|
1319
|
+
)
|
1320
|
+
|
1321
|
+
report = handle.wait(max_wait_timeout: 5)
|
1322
|
+
expect(report.partition).to be >= 0
|
1323
|
+
expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
|
1324
|
+
end
|
1325
|
+
end
|
1326
|
+
|
1327
|
+
it "should handle unicode keys with all partitioners" do
|
1328
|
+
unicode_key = "测试键值🚀"
|
1329
|
+
|
1330
|
+
all_partitioners.each do |partitioner|
|
1331
|
+
handle = producer.produce(
|
1332
|
+
topic: "partitioner_test_topic",
|
1333
|
+
payload: "test payload",
|
1334
|
+
partition_key: unicode_key,
|
1335
|
+
partitioner: partitioner
|
1336
|
+
)
|
1337
|
+
|
1338
|
+
report = handle.wait(max_wait_timeout: 5)
|
1339
|
+
expect(report.partition).to be >= 0
|
1340
|
+
expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
|
1341
|
+
end
|
1342
|
+
end
|
1343
|
+
end
|
1344
|
+
|
1345
|
+
context "consistency testing for deterministic partitioners" do
|
1346
|
+
%w(consistent murmur2 fnv1a).each do |partitioner|
|
1347
|
+
it "should consistently route same partition key to same partition with #{partitioner}" do
|
1348
|
+
partition_key = "consistent-test-key"
|
1349
|
+
|
1350
|
+
# Produce multiple messages with same partition key
|
1351
|
+
reports = 5.times.map do
|
1352
|
+
handle = producer.produce(
|
1353
|
+
topic: "partitioner_test_topic",
|
1354
|
+
payload: "test payload #{Time.now.to_f}",
|
1355
|
+
partition_key: partition_key,
|
1356
|
+
partitioner: partitioner
|
1357
|
+
)
|
1358
|
+
handle.wait(max_wait_timeout: 5)
|
1359
|
+
end
|
1360
|
+
|
1361
|
+
# All should go to same partition
|
1362
|
+
partitions = reports.map(&:partition).uniq
|
1363
|
+
expect(partitions.size).to eq(1)
|
1364
|
+
end
|
1365
|
+
end
|
1366
|
+
end
|
1367
|
+
|
1368
|
+
context "randomness testing for random partitioners" do
|
1369
|
+
%w(random consistent_random murmur2_random fnv1a_random).each do |partitioner|
|
1370
|
+
it "should potentially distribute across partitions with #{partitioner}" do
|
1371
|
+
# Note: random partitioners might still return same value by chance
|
1372
|
+
partition_key = "random-test-key"
|
1373
|
+
|
1374
|
+
reports = 10.times.map do
|
1375
|
+
handle = producer.produce(
|
1376
|
+
topic: "partitioner_test_topic",
|
1377
|
+
payload: "test payload #{Time.now.to_f}",
|
1378
|
+
partition_key: partition_key,
|
1379
|
+
partitioner: partitioner
|
1380
|
+
)
|
1381
|
+
handle.wait(max_wait_timeout: 5)
|
1382
|
+
end
|
1383
|
+
|
1384
|
+
partitions = reports.map(&:partition)
|
1385
|
+
|
1386
|
+
# Just ensure they're valid partitions
|
1387
|
+
partitions.each do |partition|
|
1388
|
+
expect(partition).to be >= 0
|
1389
|
+
expect(partition).to be < producer.partition_count("partitioner_test_topic")
|
1390
|
+
end
|
1391
|
+
end
|
1392
|
+
end
|
1393
|
+
end
|
1394
|
+
|
1395
|
+
context "comparing different partitioners with same key" do
|
1396
|
+
it "should route different partition keys to potentially different partitions" do
|
1397
|
+
keys = ["key1", "key2", "key3", "key4", "key5"]
|
1398
|
+
|
1399
|
+
all_partitioners.each do |partitioner|
|
1400
|
+
reports = keys.map do |key|
|
1401
|
+
handle = producer.produce(
|
1402
|
+
topic: "partitioner_test_topic",
|
1403
|
+
payload: "test payload",
|
1404
|
+
partition_key: key,
|
1405
|
+
partitioner: partitioner
|
1406
|
+
)
|
1407
|
+
handle.wait(max_wait_timeout: 5)
|
1408
|
+
end
|
1409
|
+
|
1410
|
+
partitions = reports.map(&:partition).uniq
|
1411
|
+
|
1412
|
+
# Should distribute across multiple partitions for most partitioners
|
1413
|
+
# (though some might hash all keys to same partition by chance)
|
1414
|
+
expect(partitions.all? { |p| p >= 0 && p < producer.partition_count("partitioner_test_topic") }).to be true
|
1415
|
+
end
|
1416
|
+
end
|
1417
|
+
end
|
1418
|
+
|
1419
|
+
context "partition key vs regular key behavior" do
|
1420
|
+
it "should use partition key for partitioning when both key and partition_key are provided" do
|
1421
|
+
# Use keys that would hash to different partitions
|
1422
|
+
regular_key = "regular-key-123"
|
1423
|
+
partition_key = "partition-key-456"
|
1424
|
+
|
1425
|
+
# Message with both keys
|
1426
|
+
handle1 = producer.produce(
|
1427
|
+
topic: "partitioner_test_topic",
|
1428
|
+
payload: "test payload 1",
|
1429
|
+
key: regular_key,
|
1430
|
+
partition_key: partition_key
|
1431
|
+
)
|
1432
|
+
|
1433
|
+
# Message with only partition key (should go to same partition)
|
1434
|
+
handle2 = producer.produce(
|
1435
|
+
topic: "partitioner_test_topic",
|
1436
|
+
payload: "test payload 2",
|
1437
|
+
partition_key: partition_key
|
1438
|
+
)
|
1439
|
+
|
1440
|
+
# Message with only regular key (should go to different partition)
|
1441
|
+
handle3 = producer.produce(
|
1442
|
+
topic: "partitioner_test_topic",
|
1443
|
+
payload: "test payload 3",
|
1444
|
+
key: regular_key
|
1445
|
+
)
|
1446
|
+
|
1447
|
+
report1 = handle1.wait(max_wait_timeout: 5)
|
1448
|
+
report2 = handle2.wait(max_wait_timeout: 5)
|
1449
|
+
report3 = handle3.wait(max_wait_timeout: 5)
|
1450
|
+
|
1451
|
+
# Messages 1 and 2 should go to same partition (both use partition_key)
|
1452
|
+
expect(report1.partition).to eq(report2.partition)
|
1453
|
+
|
1454
|
+
# Message 3 should potentially go to different partition (uses regular key)
|
1455
|
+
expect(report3.partition).not_to eq(report1.partition)
|
1456
|
+
end
|
1457
|
+
end
|
1458
|
+
|
1459
|
+
context "edge case combinations with different partitioners" do
|
1460
|
+
it "should handle nil partition key with all partitioners" do
|
1461
|
+
all_partitioners.each do |partitioner|
|
1462
|
+
handle = producer.produce(
|
1463
|
+
topic: "partitioner_test_topic",
|
1464
|
+
payload: "test payload",
|
1465
|
+
key: "test-key",
|
1466
|
+
partition_key: nil,
|
1467
|
+
partitioner: partitioner
|
1468
|
+
)
|
1469
|
+
|
1470
|
+
report = handle.wait(max_wait_timeout: 5)
|
1471
|
+
expect(report.partition).to be >= 0
|
1472
|
+
expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
|
1473
|
+
end
|
1474
|
+
end
|
1475
|
+
|
1476
|
+
it "should handle whitespace-only partition key with all partitioners" do
|
1477
|
+
all_partitioners.each do |partitioner|
|
1478
|
+
handle = producer.produce(
|
1479
|
+
topic: "partitioner_test_topic",
|
1480
|
+
payload: "test payload",
|
1481
|
+
partition_key: " ",
|
1482
|
+
partitioner: partitioner
|
1483
|
+
)
|
1484
|
+
|
1485
|
+
report = handle.wait(max_wait_timeout: 5)
|
1486
|
+
expect(report.partition).to be >= 0
|
1487
|
+
expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
|
1488
|
+
end
|
1489
|
+
end
|
1490
|
+
|
1491
|
+
it "should handle newline characters in partition key with all partitioners" do
|
1492
|
+
all_partitioners.each do |partitioner|
|
1493
|
+
handle = producer.produce(
|
1494
|
+
topic: "partitioner_test_topic",
|
1495
|
+
payload: "test payload",
|
1496
|
+
partition_key: "key\nwith\nnewlines",
|
1497
|
+
partitioner: partitioner
|
1498
|
+
)
|
1499
|
+
|
1500
|
+
report = handle.wait(max_wait_timeout: 5)
|
1501
|
+
expect(report.partition).to be >= 0
|
1502
|
+
expect(report.partition).to be < producer.partition_count("partitioner_test_topic")
|
1503
|
+
end
|
1504
|
+
end
|
1505
|
+
end
|
1506
|
+
|
1507
|
+
context "debugging partitioner issues" do
|
1508
|
+
it "should show if all partitioners return 0 (indicating a problem)" do
|
1509
|
+
test_key = "debug-test-key"
|
1510
|
+
zero_count = 0
|
1511
|
+
|
1512
|
+
all_partitioners.each do |partitioner|
|
1513
|
+
handle = producer.produce(
|
1514
|
+
topic: "partitioner_test_topic",
|
1515
|
+
payload: "debug payload",
|
1516
|
+
partition_key: test_key,
|
1517
|
+
partitioner: partitioner
|
1518
|
+
)
|
1519
|
+
|
1520
|
+
report = handle.wait(max_wait_timeout: 5)
|
1521
|
+
zero_count += 1 if report.partition == 0
|
1522
|
+
end
|
1523
|
+
|
1524
|
+
expect(zero_count).to be < all_partitioners.size
|
1525
|
+
end
|
1526
|
+
end
|
1527
|
+
end
|
1043
1528
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -15,7 +15,12 @@ require "securerandom"
|
|
15
15
|
|
16
16
|
def rdkafka_base_config
|
17
17
|
{
|
18
|
-
:"
|
18
|
+
:"api.version.request" => false,
|
19
|
+
:"broker.version.fallback" => "1.0",
|
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
|
19
24
|
}
|
20
25
|
end
|
21
26
|
|
@@ -73,18 +78,32 @@ end
|
|
73
78
|
|
74
79
|
def wait_for_message(topic:, delivery_report:, timeout_in_seconds: 30, consumer: nil)
|
75
80
|
new_consumer = consumer.nil?
|
76
|
-
consumer ||= rdkafka_consumer_config.consumer
|
81
|
+
consumer ||= rdkafka_consumer_config('allow.auto.create.topics': true).consumer
|
77
82
|
consumer.subscribe(topic)
|
78
83
|
timeout = Time.now.to_i + timeout_in_seconds
|
84
|
+
retry_count = 0
|
85
|
+
max_retries = 10
|
86
|
+
|
79
87
|
loop do
|
80
88
|
if timeout <= Time.now.to_i
|
81
89
|
raise "Timeout of #{timeout_in_seconds} seconds reached in wait_for_message"
|
82
90
|
end
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
88
107
|
end
|
89
108
|
end
|
90
109
|
ensure
|
@@ -123,6 +142,12 @@ RSpec.configure do |config|
|
|
123
142
|
config.filter_run focus: true
|
124
143
|
config.run_all_when_everything_filtered = true
|
125
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
|
+
|
126
151
|
config.before(:suite) do
|
127
152
|
admin = rdkafka_config.admin
|
128
153
|
{
|