karafka-rdkafka 0.20.0.rc2 → 0.20.0.rc5

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/workflows/ci_linux_x86_64_gnu.yml +249 -0
  3. data/.github/workflows/ci_linux_x86_64_musl.yml +205 -0
  4. data/.github/workflows/ci_macos_arm64.yml +306 -0
  5. data/.github/workflows/push_linux_x86_64_gnu.yml +64 -0
  6. data/.github/workflows/push_linux_x86_64_musl.yml +77 -0
  7. data/.github/workflows/push_macos_arm64.yml +54 -0
  8. data/.github/workflows/push_ruby.yml +37 -0
  9. data/.gitignore +1 -0
  10. data/.ruby-version +1 -1
  11. data/CHANGELOG.md +22 -3
  12. data/README.md +2 -3
  13. data/Rakefile +0 -2
  14. data/dist/{librdkafka-2.10.0.tar.gz → librdkafka-2.8.0.tar.gz} +0 -0
  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/karafka-rdkafka.gemspec +26 -10
  22. data/lib/rdkafka/bindings.rb +31 -4
  23. data/lib/rdkafka/config.rb +4 -1
  24. data/lib/rdkafka/error.rb +8 -1
  25. data/lib/rdkafka/native_kafka.rb +4 -0
  26. data/lib/rdkafka/producer/partitions_count_cache.rb +216 -0
  27. data/lib/rdkafka/producer.rb +40 -28
  28. data/lib/rdkafka/version.rb +3 -3
  29. data/lib/rdkafka.rb +1 -0
  30. data/renovate.json +74 -0
  31. data/spec/rdkafka/admin_spec.rb +15 -2
  32. data/spec/rdkafka/bindings_spec.rb +0 -1
  33. data/spec/rdkafka/config_spec.rb +1 -1
  34. data/spec/rdkafka/consumer_spec.rb +35 -14
  35. data/spec/rdkafka/metadata_spec.rb +2 -2
  36. data/spec/rdkafka/producer/partitions_count_cache_spec.rb +359 -0
  37. data/spec/rdkafka/producer/partitions_count_spec.rb +359 -0
  38. data/spec/rdkafka/producer_spec.rb +198 -7
  39. data/spec/spec_helper.rb +12 -1
  40. metadata +43 -100
  41. checksums.yaml.gz.sig +0 -0
  42. data/.github/workflows/ci.yml +0 -99
  43. data/Guardfile +0 -19
  44. data/certs/cert.pem +0 -26
  45. data.tar.gz.sig +0 -0
  46. metadata.gz.sig +0 -3
data/lib/rdkafka/error.rb CHANGED
@@ -126,7 +126,14 @@ module Rdkafka
126
126
  else
127
127
  ''
128
128
  end
129
- "#{message_prefix_part}#{Rdkafka::Bindings.rd_kafka_err2str(@rdkafka_response)} (#{code})"
129
+
130
+ err_str = Rdkafka::Bindings.rd_kafka_err2str(@rdkafka_response)
131
+ base = "#{message_prefix_part}#{err_str} (#{code})"
132
+
133
+ return base if broker_message.nil?
134
+ return base if broker_message.empty?
135
+
136
+ "#{base}\n#{broker_message}"
130
137
  end
131
138
 
132
139
  # Whether this error indicates the partition is EOF.
@@ -126,9 +126,13 @@ module Rdkafka
126
126
  # and would continue to run, trying to destroy inner twice
127
127
  return unless @inner
128
128
 
129
+ yield if block_given?
130
+
129
131
  Rdkafka::Bindings.rd_kafka_destroy(@inner)
130
132
  @inner = nil
131
133
  @opaque = nil
134
+ @poll_mutex.unlock
135
+ @poll_mutex = nil
132
136
  end
133
137
  end
134
138
  end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ class Producer
5
+ # Caching mechanism for Kafka topic partition counts to avoid frequent cluster queries
6
+ #
7
+ # This cache is designed to optimize the process of obtaining partition counts for topics.
8
+ # It uses several strategies to minimize Kafka cluster queries:
9
+ #
10
+ # @note Design considerations:
11
+ #
12
+ # 1. Statistics-based updates
13
+ # When statistics callbacks are enabled (via `statistics.interval.ms`), we leverage
14
+ # this data to proactively update the partition counts cache. This approach costs
15
+ # approximately 0.02ms of processing time during each statistics interval (typically
16
+ # every 5 seconds) but eliminates the need for explicit blocking metadata queries.
17
+ #
18
+ # 2. Edge case handling
19
+ # If a user configures `statistics.interval.ms` much higher than the default cache TTL
20
+ # (30 seconds), the cache will still function correctly. When statistics updates don't
21
+ # occur frequently enough, the cache entries will expire naturally, triggering a
22
+ # blocking refresh when needed.
23
+ #
24
+ # 3. User configuration awareness
25
+ # The cache respects user-defined settings. If `topic.metadata.refresh.interval.ms` is
26
+ # set very high, the responsibility for potentially stale data falls on the user. This
27
+ # is an explicit design choice to honor user configuration preferences and align with
28
+ # librdkafka settings.
29
+ #
30
+ # 4. Process-wide efficiency
31
+ # Since this cache is shared across all Rdkafka producers and consumers within a process,
32
+ # having multiple clients improves overall efficiency. Each client contributes to keeping
33
+ # the cache updated, benefiting all other clients.
34
+ #
35
+ # 5. Thread-safety approach
36
+ # The implementation uses fine-grained locking with per-topic mutexes to minimize
37
+ # contention in multi-threaded environments while ensuring data consistency.
38
+ #
39
+ # 6. Topic recreation handling
40
+ # If a topic is deleted and recreated with fewer partitions, the cache will continue to
41
+ # report the higher count until either the TTL expires or the process is restarted. This
42
+ # design choice simplifies the implementation while relying on librdkafka's error handling
43
+ # for edge cases. In production environments, topic recreation with different partition
44
+ # counts is typically accompanied by application restarts to handle structural changes.
45
+ # This also aligns with the previous cache implementation.
46
+ class PartitionsCountCache
47
+ include Helpers::Time
48
+
49
+ # Default time-to-live for cached partition counts in seconds
50
+ #
51
+ # @note This default was chosen to balance freshness of metadata with performance
52
+ # optimization. Most Kafka cluster topology changes are planned operations, making 30
53
+ # seconds a reasonable compromise.
54
+ DEFAULT_TTL = 30
55
+
56
+ # Creates a new partition count cache
57
+ #
58
+ # @param ttl [Integer] Time-to-live in seconds for cached values
59
+ def initialize(ttl = DEFAULT_TTL)
60
+ @counts = {}
61
+ @mutex_hash = {}
62
+ # Used only for @mutex_hash access to ensure thread-safety when creating new mutexes
63
+ @mutex_for_hash = Mutex.new
64
+ @ttl = ttl
65
+ end
66
+
67
+ # Reads partition count for a topic with automatic refresh when expired
68
+ #
69
+ # This method will return the cached partition count if available and not expired.
70
+ # If the value is expired or not available, it will execute the provided block
71
+ # to fetch the current value from Kafka.
72
+ #
73
+ # @param topic [String] Kafka topic name
74
+ # @yield Block that returns the current partition count when cache needs refreshing
75
+ # @yieldreturn [Integer] Current partition count retrieved from Kafka
76
+ # @return [Integer] Partition count for the topic
77
+ #
78
+ # @note The implementation prioritizes read performance over write consistency
79
+ # since partition counts typically only increase during normal operation.
80
+ def get(topic)
81
+ current_info = @counts[topic]
82
+
83
+ if current_info.nil? || expired?(current_info[0])
84
+ new_count = yield
85
+
86
+ if current_info.nil?
87
+ # No existing data, create a new entry with mutex
88
+ set(topic, new_count)
89
+
90
+ return new_count
91
+ else
92
+ current_count = current_info[1]
93
+
94
+ if new_count > current_count
95
+ # Higher value needs mutex to update both timestamp and count
96
+ set(topic, new_count)
97
+
98
+ return new_count
99
+ else
100
+ # Same or lower value, just update timestamp without mutex
101
+ refresh_timestamp(topic)
102
+
103
+ return current_count
104
+ end
105
+ end
106
+ end
107
+
108
+ current_info[1]
109
+ end
110
+
111
+ # Update partition count for a topic when needed
112
+ #
113
+ # This method updates the partition count for a topic in the cache.
114
+ # It uses a mutex to ensure thread-safety during updates.
115
+ #
116
+ # @param topic [String] Kafka topic name
117
+ # @param new_count [Integer] New partition count value
118
+ #
119
+ # @note We prioritize higher partition counts and only accept them when using
120
+ # a mutex to ensure consistency. This design decision is based on the fact that
121
+ # partition counts in Kafka only increase during normal operation.
122
+ def set(topic, new_count)
123
+ # First check outside mutex to avoid unnecessary locking
124
+ current_info = @counts[topic]
125
+
126
+ # For lower values, we don't update count but might need to refresh timestamp
127
+ if current_info && new_count < current_info[1]
128
+ refresh_timestamp(topic)
129
+
130
+ return
131
+ end
132
+
133
+ # Only lock the specific topic mutex
134
+ mutex_for(topic).synchronize do
135
+ # Check again inside the lock as another thread might have updated
136
+ current_info = @counts[topic]
137
+
138
+ if current_info.nil?
139
+ # Create new entry
140
+ @counts[topic] = [monotonic_now, new_count]
141
+ else
142
+ current_count = current_info[1]
143
+
144
+ if new_count > current_count
145
+ # Update to higher count value
146
+ current_info[0] = monotonic_now
147
+ current_info[1] = new_count
148
+ else
149
+ # Same or lower count, update timestamp only
150
+ current_info[0] = monotonic_now
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ # @return [Hash] hash with ttls and partitions counts array
157
+ def to_h
158
+ @counts
159
+ end
160
+
161
+ private
162
+
163
+ # Get or create a mutex for a specific topic
164
+ #
165
+ # This method ensures that each topic has its own mutex,
166
+ # allowing operations on different topics to proceed in parallel.
167
+ #
168
+ # @param topic [String] Kafka topic name
169
+ # @return [Mutex] Mutex for the specified topic
170
+ #
171
+ # @note We use a separate mutex (@mutex_for_hash) to protect the creation
172
+ # of new topic mutexes. This pattern allows fine-grained locking while
173
+ # maintaining thread-safety.
174
+ def mutex_for(topic)
175
+ mutex = @mutex_hash[topic]
176
+
177
+ return mutex if mutex
178
+
179
+ # Use a separate mutex to protect the creation of new topic mutexes
180
+ @mutex_for_hash.synchronize do
181
+ # Check again in case another thread created it
182
+ @mutex_hash[topic] ||= Mutex.new
183
+ end
184
+
185
+ @mutex_hash[topic]
186
+ end
187
+
188
+ # Update the timestamp without acquiring the mutex
189
+ #
190
+ # This is an optimization that allows refreshing the TTL of existing entries
191
+ # without the overhead of mutex acquisition.
192
+ #
193
+ # @param topic [String] Kafka topic name
194
+ #
195
+ # @note This method is safe for refreshing existing data regardless of count
196
+ # because it only updates the timestamp, which doesn't affect the correctness
197
+ # of concurrent operations.
198
+ def refresh_timestamp(topic)
199
+ current_info = @counts[topic]
200
+
201
+ return unless current_info
202
+
203
+ # Update the timestamp in-place
204
+ current_info[0] = monotonic_now
205
+ end
206
+
207
+ # Check if a timestamp has expired based on the TTL
208
+ #
209
+ # @param timestamp [Float] Monotonic timestamp to check
210
+ # @return [Boolean] true if expired, false otherwise
211
+ def expired?(timestamp)
212
+ monotonic_now - timestamp > @ttl
213
+ end
214
+ end
215
+ end
216
+ end
@@ -6,13 +6,31 @@ module Rdkafka
6
6
  include Helpers::Time
7
7
  include Helpers::OAuth
8
8
 
9
- # Cache partitions count for 30 seconds
10
- PARTITIONS_COUNT_TTL = 30
9
+ # @private
10
+ @@partitions_count_cache = PartitionsCountCache.new
11
+
12
+ # Global (process wide) partitions cache. We use it to store number of topics partitions,
13
+ # either from the librdkafka statistics (if enabled) or via direct inline calls every now and
14
+ # then. Since the partitions count can only grow and should be same for all consumers and
15
+ # producers, we can use a global cache as long as we ensure that updates only move up.
16
+ #
17
+ # @note It is critical to remember, that not all users may have statistics callbacks enabled,
18
+ # hence we should not make assumption that this cache is always updated from the stats.
19
+ #
20
+ # @return [Rdkafka::Producer::PartitionsCountCache]
21
+ def self.partitions_count_cache
22
+ @@partitions_count_cache
23
+ end
24
+
25
+ # @param partitions_count_cache [Rdkafka::Producer::PartitionsCountCache]
26
+ def self.partitions_count_cache=(partitions_count_cache)
27
+ @@partitions_count_cache = partitions_count_cache
28
+ end
11
29
 
12
30
  # Empty hash used as a default
13
31
  EMPTY_HASH = {}.freeze
14
32
 
15
- private_constant :PARTITIONS_COUNT_TTL, :EMPTY_HASH
33
+ private_constant :EMPTY_HASH
16
34
 
17
35
  # Raised when there was a critical issue when invoking rd_kafka_topic_new
18
36
  # This is a temporary solution until https://github.com/karafka/rdkafka-ruby/issues/451 is
@@ -43,25 +61,6 @@ module Rdkafka
43
61
 
44
62
  # Makes sure, that native kafka gets closed before it gets GCed by Ruby
45
63
  ObjectSpace.define_finalizer(self, native_kafka.finalizer)
46
-
47
- @_partitions_count_cache = Hash.new do |cache, topic|
48
- topic_metadata = nil
49
-
50
- @native_kafka.with_inner do |inner|
51
- topic_metadata = ::Rdkafka::Metadata.new(inner, topic).topics&.first
52
- end
53
-
54
- partition_count = topic_metadata ? topic_metadata[:partition_count] : -1
55
-
56
- # This approach caches the failure to fetch only for 1 second. This will make sure, that
57
- # we do not cache the failure for too long but also "buys" us a bit of time in case there
58
- # would be issues in the cluster so we won't overaload it with consecutive requests
59
- cache[topic] = if partition_count.positive?
60
- [monotonic_now, partition_count]
61
- else
62
- [monotonic_now - PARTITIONS_COUNT_TTL + 5, partition_count]
63
- end
64
- end
65
64
  end
66
65
 
67
66
  # Sets alternative set of configuration details that can be set per topic
@@ -284,18 +283,31 @@ module Rdkafka
284
283
  # @note If 'allow.auto.create.topics' is set to true in the broker, the topic will be
285
284
  # auto-created after returning nil.
286
285
  #
287
- # @note We cache the partition count for a given topic for given time.
286
+ # @note We cache the partition count for a given topic for given time. If statistics are
287
+ # enabled for any producer or consumer, it will take precedence over per instance fetching.
288
+ #
288
289
  # This prevents us in case someone uses `partition_key` from querying for the count with
289
- # each message. Instead we query once every 30 seconds at most if we have a valid partition
290
- # count or every 5 seconds in case we were not able to obtain number of partitions
290
+ # each message. Instead we query at most once every 30 seconds at most if we have a valid
291
+ # partition count or every 5 seconds in case we were not able to obtain number of partitions.
291
292
  def partition_count(topic)
292
293
  closed_producer_check(__method__)
293
294
 
294
- @_partitions_count_cache.delete_if do |_, cached|
295
- monotonic_now - cached.first > PARTITIONS_COUNT_TTL
295
+ self.class.partitions_count_cache.get(topic) do
296
+ topic_metadata = nil
297
+
298
+ @native_kafka.with_inner do |inner|
299
+ topic_metadata = ::Rdkafka::Metadata.new(inner, topic).topics&.first
300
+ end
301
+
302
+ topic_metadata ? topic_metadata[:partition_count] : -1
296
303
  end
304
+ rescue Rdkafka::RdkafkaError => e
305
+ # If the topic does not exist, it will be created or if not allowed another error will be
306
+ # raised. We here return -1 so this can happen without early error happening on metadata
307
+ # discovery.
308
+ return -1 if e.code == :unknown_topic_or_part
297
309
 
298
- @_partitions_count_cache[topic].last
310
+ raise(e)
299
311
  end
300
312
 
301
313
  # Produces a message to a Kafka topic. The message is added to rdkafka's queue, call {DeliveryHandle#wait wait} on the returned delivery handle to make sure it is delivered.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdkafka
4
- VERSION = "0.20.0.rc2"
5
- LIBRDKAFKA_VERSION = "2.10.0"
6
- LIBRDKAFKA_SOURCE_SHA256 = "004b1cc2685d1d6d416b90b426a0a9d27327a214c6b807df6f9ea5887346ba3a"
4
+ VERSION = "0.20.0.rc5"
5
+ LIBRDKAFKA_VERSION = "2.8.0"
6
+ LIBRDKAFKA_SOURCE_SHA256 = "5bd1c46f63265f31c6bfcedcde78703f77d28238eadf23821c2b43fc30be3e25"
7
7
  end
data/lib/rdkafka.rb CHANGED
@@ -42,6 +42,7 @@ require "rdkafka/consumer/topic_partition_list"
42
42
  require "rdkafka/error"
43
43
  require "rdkafka/metadata"
44
44
  require "rdkafka/native_kafka"
45
+ require "rdkafka/producer/partitions_count_cache"
45
46
  require "rdkafka/producer"
46
47
  require "rdkafka/producer/delivery_handle"
47
48
  require "rdkafka/producer/delivery_report"
data/renovate.json CHANGED
@@ -14,5 +14,79 @@
14
14
  ],
15
15
  "minimumReleaseAge": "7 days"
16
16
  }
17
+ ],
18
+ "customManagers": [
19
+ {
20
+ "customType": "regex",
21
+ "managerFilePatterns": [
22
+ "/^ext/build_common\\.sh$/"
23
+ ],
24
+ "matchStrings": [
25
+ "readonly OPENSSL_VERSION=\"(?<currentValue>.*)\""
26
+ ],
27
+ "depNameTemplate": "openssl/openssl",
28
+ "datasourceTemplate": "github-releases",
29
+ "extractVersionTemplate": "^OpenSSL_(?<version>.*)$"
30
+ },
31
+ {
32
+ "customType": "regex",
33
+ "managerFilePatterns": [
34
+ "/^ext/build_common\\.sh$/"
35
+ ],
36
+ "matchStrings": [
37
+ "readonly CYRUS_SASL_VERSION=\"(?<currentValue>.*)\""
38
+ ],
39
+ "depNameTemplate": "cyrusimap/cyrus-sasl",
40
+ "datasourceTemplate": "github-releases",
41
+ "extractVersionTemplate": "^cyrus-sasl-(?<version>.*)$"
42
+ },
43
+ {
44
+ "customType": "regex",
45
+ "managerFilePatterns": [
46
+ "/^ext/build_common\\.sh$/"
47
+ ],
48
+ "matchStrings": [
49
+ "readonly ZLIB_VERSION=\"(?<currentValue>.*)\""
50
+ ],
51
+ "depNameTemplate": "madler/zlib",
52
+ "datasourceTemplate": "github-releases",
53
+ "extractVersionTemplate": "^v(?<version>.*)$"
54
+ },
55
+ {
56
+ "customType": "regex",
57
+ "managerFilePatterns": [
58
+ "/^ext/build_common\\.sh$/"
59
+ ],
60
+ "matchStrings": [
61
+ "readonly ZSTD_VERSION=\"(?<currentValue>.*)\""
62
+ ],
63
+ "depNameTemplate": "facebook/zstd",
64
+ "datasourceTemplate": "github-releases",
65
+ "extractVersionTemplate": "^v(?<version>.*)$"
66
+ },
67
+ {
68
+ "customType": "regex",
69
+ "managerFilePatterns": [
70
+ "/^ext/build_common\\.sh$/"
71
+ ],
72
+ "matchStrings": [
73
+ "readonly KRB5_VERSION=\"(?<currentValue>.*)\""
74
+ ],
75
+ "depNameTemplate": "krb5/krb5",
76
+ "datasourceTemplate": "github-releases",
77
+ "extractVersionTemplate": "^krb5-(?<version>.*)$"
78
+ },
79
+ {
80
+ "customType": "regex",
81
+ "managerFilePatterns": [
82
+ "/^ext/build_common\\.sh$/"
83
+ ],
84
+ "matchStrings": [
85
+ "readonly LIBRDKAFKA_VERSION=\"(?<currentValue>.*)\""
86
+ ],
87
+ "depNameTemplate": "confluentinc/librdkafka",
88
+ "datasourceTemplate": "github-releases",
89
+ "extractVersionTemplate": "^v(?<version>.*)$"
90
+ }
17
91
  ]
18
92
  }
@@ -34,7 +34,7 @@ describe Rdkafka::Admin do
34
34
  describe '#describe_errors' do
35
35
  let(:errors) { admin.class.describe_errors }
36
36
 
37
- it { expect(errors.size).to eq(172) }
37
+ it { expect(errors.size).to eq(170) }
38
38
  it { expect(errors[-184]).to eq(code: -184, description: 'Local: Queue full', name: '_QUEUE_FULL') }
39
39
  it { expect(errors[21]).to eq(code: 21, description: 'Broker: Invalid required acks value', name: 'INVALID_REQUIRED_ACKS') }
40
40
  end
@@ -295,6 +295,8 @@ describe Rdkafka::Admin do
295
295
  expect(resources_results.first.type).to eq(2)
296
296
  expect(resources_results.first.name).to eq(topic_name)
297
297
 
298
+ sleep(1)
299
+
298
300
  ret_config = admin.describe_configs(resources_with_configs).wait.resources.first.configs.find do |config|
299
301
  config.name == 'delete.retention.ms'
300
302
  end
@@ -325,6 +327,9 @@ describe Rdkafka::Admin do
325
327
  expect(resources_results.size).to eq(1)
326
328
  expect(resources_results.first.type).to eq(2)
327
329
  expect(resources_results.first.name).to eq(topic_name)
330
+
331
+ sleep(1)
332
+
328
333
  ret_config = admin.describe_configs(resources_with_configs).wait.resources.first.configs.find do |config|
329
334
  config.name == 'delete.retention.ms'
330
335
  end
@@ -356,6 +361,8 @@ describe Rdkafka::Admin do
356
361
  expect(resources_results.first.type).to eq(2)
357
362
  expect(resources_results.first.name).to eq(topic_name)
358
363
 
364
+ sleep(1)
365
+
359
366
  ret_config = admin.describe_configs(resources_with_configs).wait.resources.first.configs.find do |config|
360
367
  config.name == 'cleanup.policy'
361
368
  end
@@ -387,6 +394,8 @@ describe Rdkafka::Admin do
387
394
  expect(resources_results.first.type).to eq(2)
388
395
  expect(resources_results.first.name).to eq(topic_name)
389
396
 
397
+ sleep(1)
398
+
390
399
  ret_config = admin.describe_configs(resources_with_configs).wait.resources.first.configs.find do |config|
391
400
  config.name == 'cleanup.policy'
392
401
  end
@@ -622,7 +631,11 @@ describe Rdkafka::Admin do
622
631
 
623
632
  consumer.subscribe(topic_name)
624
633
  wait_for_assignment(consumer)
625
- message = consumer.poll(100)
634
+ message = nil
635
+
636
+ 10.times do
637
+ message ||= consumer.poll(100)
638
+ end
626
639
 
627
640
  expect(message).to_not be_nil
628
641
 
@@ -194,7 +194,6 @@ describe Rdkafka::Bindings do
194
194
  end
195
195
 
196
196
  describe "oauthbearer callback" do
197
-
198
197
  context "without an oauthbearer callback" do
199
198
  it "should do nothing" do
200
199
  expect {
@@ -159,7 +159,7 @@ describe Rdkafka::Config do
159
159
 
160
160
  it "should use default configuration" do
161
161
  config = Rdkafka::Config.new
162
- expect(config[:"api.version.request"]).to eq nil
162
+ expect(config[:"api.version.request"]).to eq true
163
163
  end
164
164
 
165
165
  it "should create a consumer with valid config" do
@@ -170,8 +170,16 @@ describe Rdkafka::Consumer do
170
170
  end
171
171
 
172
172
  describe "#seek" do
173
+ let(:topic) { "it-#{SecureRandom.uuid}" }
174
+
175
+ before do
176
+ admin = rdkafka_producer_config.admin
177
+ admin.create_topic(topic, 1, 1).wait
178
+ admin.close
179
+ end
180
+
173
181
  it "should raise an error when seeking fails" do
174
- fake_msg = OpenStruct.new(topic: "consume_test_topic", partition: 0, offset: 0)
182
+ fake_msg = OpenStruct.new(topic: topic, partition: 0, offset: 0)
175
183
 
176
184
  expect(Rdkafka::Bindings).to receive(:rd_kafka_seek).and_return(20)
177
185
  expect {
@@ -181,9 +189,12 @@ describe Rdkafka::Consumer do
181
189
 
182
190
  context "subscription" do
183
191
  let(:timeout) { 1000 }
192
+ # Some specs here test the manual offset commit hence we want to ensure, that we have some
193
+ # offsets in-memory that we can manually commit
194
+ let(:consumer) { rdkafka_consumer_config('auto.commit.interval.ms': 60_000).consumer }
184
195
 
185
196
  before do
186
- consumer.subscribe("consume_test_topic")
197
+ consumer.subscribe(topic)
187
198
 
188
199
  # 1. partitions are assigned
189
200
  wait_for_assignment(consumer)
@@ -196,7 +207,7 @@ describe Rdkafka::Consumer do
196
207
 
197
208
  def send_one_message(val)
198
209
  producer.produce(
199
- topic: "consume_test_topic",
210
+ topic: topic,
200
211
  payload: "payload #{val}",
201
212
  key: "key 1",
202
213
  partition: 0
@@ -211,7 +222,7 @@ describe Rdkafka::Consumer do
211
222
 
212
223
  # 4. pause the subscription
213
224
  tpl = Rdkafka::Consumer::TopicPartitionList.new
214
- tpl.add_topic("consume_test_topic", 1)
225
+ tpl.add_topic(topic, 1)
215
226
  consumer.pause(tpl)
216
227
 
217
228
  # 5. seek to previous message
@@ -219,7 +230,7 @@ describe Rdkafka::Consumer do
219
230
 
220
231
  # 6. resume the subscription
221
232
  tpl = Rdkafka::Consumer::TopicPartitionList.new
222
- tpl.add_topic("consume_test_topic", 1)
233
+ tpl.add_topic(topic, 1)
223
234
  consumer.resume(tpl)
224
235
 
225
236
  # 7. ensure same message is read again
@@ -227,7 +238,7 @@ describe Rdkafka::Consumer do
227
238
 
228
239
  # This is needed because `enable.auto.offset.store` is true but when running in CI that
229
240
  # is overloaded, offset store lags
230
- sleep(2)
241
+ sleep(1)
231
242
 
232
243
  consumer.commit
233
244
  expect(message1.offset).to eq message2.offset
@@ -259,10 +270,17 @@ describe Rdkafka::Consumer do
259
270
  end
260
271
 
261
272
  describe "#seek_by" do
262
- let(:topic) { "consume_test_topic" }
273
+ let(:consumer) { rdkafka_consumer_config('auto.commit.interval.ms': 60_000).consumer }
274
+ let(:topic) { "it-#{SecureRandom.uuid}" }
263
275
  let(:partition) { 0 }
264
276
  let(:offset) { 0 }
265
277
 
278
+ before do
279
+ admin = rdkafka_producer_config.admin
280
+ admin.create_topic(topic, 1, 1).wait
281
+ admin.close
282
+ end
283
+
266
284
  it "should raise an error when seeking fails" do
267
285
  expect(Rdkafka::Bindings).to receive(:rd_kafka_seek).and_return(20)
268
286
  expect {
@@ -283,6 +301,7 @@ describe Rdkafka::Consumer do
283
301
  # 2. eat unrelated messages
284
302
  while(consumer.poll(timeout)) do; end
285
303
  end
304
+
286
305
  after { consumer.unsubscribe }
287
306
 
288
307
  def send_one_message(val)
@@ -813,12 +832,14 @@ describe Rdkafka::Consumer do
813
832
  end
814
833
 
815
834
  it "should return a message if there is one" do
835
+ topic = "it-#{SecureRandom.uuid}"
836
+
816
837
  producer.produce(
817
- topic: "consume_test_topic",
838
+ topic: topic,
818
839
  payload: "payload 1",
819
840
  key: "key 1"
820
841
  ).wait
821
- consumer.subscribe("consume_test_topic")
842
+ consumer.subscribe(topic)
822
843
  message = consumer.each {|m| break m}
823
844
 
824
845
  expect(message).to be_a Rdkafka::Consumer::Message
@@ -838,13 +859,13 @@ describe Rdkafka::Consumer do
838
859
  }.to raise_error Rdkafka::RdkafkaError
839
860
  end
840
861
 
841
- it "expect not to raise error when polling non-existing topic" do
862
+ it "expect to raise error when polling non-existing topic" do
842
863
  missing_topic = SecureRandom.uuid
843
864
  consumer.subscribe(missing_topic)
844
865
 
845
- # @note it used to raise "Subscribed topic not available" in previous librdkafka versions
846
- # but this behaviour has been changed
847
- expect { consumer.poll(1_000) }.not_to raise_error
866
+ expect {
867
+ consumer.poll(1_000)
868
+ }.to raise_error Rdkafka::RdkafkaError, /Subscribed topic not available: #{missing_topic}/
848
869
  end
849
870
  end
850
871
 
@@ -1027,7 +1048,7 @@ describe Rdkafka::Consumer do
1027
1048
  after { Rdkafka::Config.statistics_callback = nil }
1028
1049
 
1029
1050
  let(:consumer) do
1030
- config = rdkafka_consumer_config('statistics.interval.ms': 100)
1051
+ config = rdkafka_consumer_config('statistics.interval.ms': 500)
1031
1052
  config.consumer_poll_set = false
1032
1053
  config.consumer
1033
1054
  end
@@ -30,7 +30,7 @@ describe Rdkafka::Metadata do
30
30
  it "#brokers returns our single broker" do
31
31
  expect(subject.brokers.length).to eq(1)
32
32
  expect(subject.brokers[0][:broker_id]).to eq(1)
33
- expect(subject.brokers[0][:broker_name]).to eq("127.0.0.1")
33
+ expect(%w[127.0.0.1 localhost]).to include(subject.brokers[0][:broker_name])
34
34
  expect(subject.brokers[0][:broker_port]).to eq(9092)
35
35
  end
36
36
 
@@ -53,7 +53,7 @@ describe Rdkafka::Metadata do
53
53
  it "#brokers returns our single broker" do
54
54
  expect(subject.brokers.length).to eq(1)
55
55
  expect(subject.brokers[0][:broker_id]).to eq(1)
56
- expect(subject.brokers[0][:broker_name]).to eq("127.0.0.1")
56
+ expect(%w[127.0.0.1 localhost]).to include(subject.brokers[0][:broker_name])
57
57
  expect(subject.brokers[0][:broker_port]).to eq(9092)
58
58
  end
59
59