karafka-rdkafka 0.15.0 → 0.15.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8d87787ad144c70fc58e6f70d50581e2ad4e1324089f3b0ebf6c27e42e000ad
4
- data.tar.gz: 400929c4f6ae6d617039a716d47088d18a17e6922e93b1e699ebc1755ceeeafa
3
+ metadata.gz: 8581d403b8949abb8f13bc4e0584481bb84c6a139155030670199214b1d4bd91
4
+ data.tar.gz: 5a1237797790d1be1f43356fa1975ddaab86517b135724c599dd90aa546d7348
5
5
  SHA512:
6
- metadata.gz: 4b97d0a84d0d0e59d0482533b9e6ca93e91c5963910d6f9b3e4a8f5004fc968fc9a5a0042c65c2f32902de7a38b1126fbc31c296bb913df1e2eb876057dad5f3
7
- data.tar.gz: da3ce8ebf3d1726d23f87cc28f2f7f4ede47afc1309dd098212fcab7c0d0b89bf33956282e45669b21a5251c93e89cbd73f13ae02a085b89b7695e035092c187
6
+ metadata.gz: cb31bb629da594a562083cac971cbf70f3366e4efdd2b1a542f453d53a81fca9f054e80eee5a4d50bfc86f9b820dd2cc4bc6886f3cb5a39f8217c3936f05200d
7
+ data.tar.gz: c6bace9ddc21367673887408d387eb3fe14a81aca2cf627c2f41d9163aee9b41d88ef68bbf88bf1f8097b35de00aa66d8c5cdabc300435db970dc93581d1a46e
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,15 +1,14 @@
1
1
  # Rdkafka Changelog
2
2
 
3
+ ## 0.15.1 (2024-05-09)
4
+ - **[Feature]** Provide ability to use topic config on a producer for custom behaviors per dispatch.
5
+ - [Enhancement] Use topic config reference cache for messages production to prevent topic objects allocation with each message.
6
+ - [Enhancement] Provide `Rrdkafka::Admin#describe_errors` to get errors descriptions (mensfeld)
7
+
3
8
  ## 0.15.0 (2024-04-26)
4
9
  - **[Feature]** Oauthbearer token refresh callback (bruce-szalwinski-he)
5
10
  - **[Feature]** Support incremental config describe + alter API (mensfeld)
6
11
  - [Enhancement] name polling Thread as `rdkafka.native_kafka#<name>` (nijikon)
7
- - [Enhancement] Replace time poll based wait engine with an event based to improve response times on blocking operations and wait (nijikon + mensfeld)
8
- - [Enhancement] Allow for usage of the second regex engine of librdkafka by setting `RDKAFKA_DISABLE_REGEX_EXT` during build (mensfeld)
9
- - [Enhancement] name polling Thread as `rdkafka.native_kafka#<name>` (nijikon)
10
- - [Change] Allow for native kafka thread operations deferring and manual start for consumer, producer and admin.
11
- - [Change] The `wait_timeout` argument in `AbstractHandle.wait` method is deprecated and will be removed in future versions without replacement. We don't rely on it's value anymore (nijikon)
12
- - [Fix] Fix bogus case/when syntax. Levels 1, 2, and 6 previously defaulted to UNKNOWN (jjowdy)
13
12
 
14
13
  ## 0.14.10 (2024-02-08)
15
14
  - [Fix] Background logger stops working after forking causing memory leaks (mensfeld).
data/lib/rdkafka/admin.rb CHANGED
@@ -4,6 +4,50 @@ module Rdkafka
4
4
  class Admin
5
5
  include Helpers::OAuth
6
6
 
7
+ class << self
8
+ # Allows us to retrieve librdkafka errors with descriptions
9
+ # Useful for debugging and building UIs, etc.
10
+ #
11
+ # @return [Hash<Integer, Hash>] hash with errors mapped by code
12
+ def describe_errors
13
+ # Memory pointers for the array of structures and count
14
+ p_error_descs = FFI::MemoryPointer.new(:pointer)
15
+ p_count = FFI::MemoryPointer.new(:size_t)
16
+
17
+ # Call the attached function
18
+ Bindings.rd_kafka_get_err_descs(p_error_descs, p_count)
19
+
20
+ # Retrieve the number of items in the array
21
+ count = p_count.read_uint
22
+
23
+ # Get the pointer to the array of error descriptions
24
+ array_of_errors = FFI::Pointer.new(Bindings::NativeErrorDesc, p_error_descs.read_pointer)
25
+
26
+ errors = {}
27
+
28
+ count.times do |i|
29
+ # Get the pointer to each struct
30
+ error_ptr = array_of_errors[i]
31
+
32
+ # Create a new instance of NativeErrorDesc for each item
33
+ error_desc = Bindings::NativeErrorDesc.new(error_ptr)
34
+
35
+ # Read values from the struct
36
+ code = error_desc[:code]
37
+
38
+ name = ''
39
+ desc = ''
40
+
41
+ name = error_desc[:name].read_string unless error_desc[:name].null?
42
+ desc = error_desc[:desc].read_string unless error_desc[:desc].null?
43
+
44
+ errors[code] = { code: code, name: name, description: desc }
45
+ end
46
+
47
+ errors
48
+ end
49
+ end
50
+
7
51
  # @private
8
52
  def initialize(native_kafka)
9
53
  @native_kafka = native_kafka
@@ -141,6 +141,11 @@ module Rdkafka
141
141
  RD_KAFKA_ALTER_CONFIG_OP_TYPE_SUBTRACT = 3
142
142
 
143
143
  # Errors
144
+ class NativeErrorDesc < FFI::Struct
145
+ layout :code, :int,
146
+ :name, :pointer,
147
+ :desc, :pointer
148
+ end
144
149
 
145
150
  attach_function :rd_kafka_err2name, [:int], :string
146
151
  attach_function :rd_kafka_err2str, [:int], :string
@@ -149,6 +154,7 @@ module Rdkafka
149
154
  attach_function :rd_kafka_error_txn_requires_abort, [:pointer], :int
150
155
  attach_function :rd_kafka_error_destroy, [:pointer], :void
151
156
  attach_function :rd_kafka_error_code, [:pointer], :int
157
+ attach_function :rd_kafka_get_err_descs, [:pointer, :pointer], :void
152
158
 
153
159
  # Configuration
154
160
 
@@ -175,6 +181,9 @@ module Rdkafka
175
181
  # Log queue
176
182
  attach_function :rd_kafka_set_log_queue, [:pointer, :pointer], :void
177
183
  attach_function :rd_kafka_queue_get_main, [:pointer], :pointer
184
+ # Per topic configs
185
+ attach_function :rd_kafka_topic_conf_new, [], :pointer
186
+ attach_function :rd_kafka_topic_conf_set, [:pointer, :string, :string, :pointer, :int], :kafka_config_response
178
187
 
179
188
  LogCallback = FFI::Function.new(
180
189
  :void, [:pointer, :int, :string, :string]
@@ -9,7 +9,15 @@ module Rdkafka
9
9
  # Cache partitions count for 30 seconds
10
10
  PARTITIONS_COUNT_TTL = 30
11
11
 
12
- private_constant :PARTITIONS_COUNT_TTL
12
+ # Empty hash used as a default
13
+ EMPTY_HASH = {}.freeze
14
+
15
+ private_constant :PARTITIONS_COUNT_TTL, :EMPTY_HASH
16
+
17
+ # Raised when there was a critical issue when invoking rd_kafka_topic_new
18
+ # This is a temporary solution until https://github.com/karafka/rdkafka-ruby/issues/451 is
19
+ # resolved and this is normalized in all the places
20
+ class TopicHandleCreationError < RuntimeError; end
13
21
 
14
22
  # @private
15
23
  # Returns the current delivery callback, by default this is nil.
@@ -28,6 +36,8 @@ module Rdkafka
28
36
  # @param partitioner_name [String, nil] name of the partitioner we want to use or nil to use
29
37
  # the "consistent_random" default
30
38
  def initialize(native_kafka, partitioner_name)
39
+ @topics_refs_map = {}
40
+ @topics_configs = {}
31
41
  @native_kafka = native_kafka
32
42
  @partitioner_name = partitioner_name || "consistent_random"
33
43
 
@@ -54,6 +64,52 @@ module Rdkafka
54
64
  end
55
65
  end
56
66
 
67
+ # Sets alternative set of configuration details that can be set per topic
68
+ # @note It is not allowed to re-set the same topic config twice because of the underlying
69
+ # librdkafka caching
70
+ # @param topic [String] The topic name
71
+ # @param config [Hash] config we want to use per topic basis
72
+ # @param config_hash [Integer] hash of the config. We expect it here instead of computing it,
73
+ # because it is already computed during the retrieval attempt in the `#produce` flow.
74
+ def set_topic_config(topic, config, config_hash)
75
+ # Ensure lock on topic reference just in case
76
+ @native_kafka.with_inner do |inner|
77
+ @topics_refs_map[topic] ||= {}
78
+ @topics_configs[topic] ||= {}
79
+
80
+ return if @topics_configs[topic].key?(config_hash)
81
+
82
+ # If config is empty, we create an empty reference that will be used with defaults
83
+ rd_topic_config = if config.empty?
84
+ nil
85
+ else
86
+ Rdkafka::Bindings.rd_kafka_topic_conf_new.tap do |topic_config|
87
+ config.each do |key, value|
88
+ error_buffer = FFI::MemoryPointer.new(:char, 256)
89
+ result = Rdkafka::Bindings.rd_kafka_topic_conf_set(
90
+ topic_config,
91
+ key.to_s,
92
+ value.to_s,
93
+ error_buffer,
94
+ 256
95
+ )
96
+
97
+ unless result == :config_ok
98
+ raise Config::ConfigError.new(error_buffer.read_string)
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ topic_handle = Bindings.rd_kafka_topic_new(inner, topic, rd_topic_config)
105
+
106
+ raise TopicHandleCreationError.new("Error creating topic handle for topic #{topic}") if topic_handle.null?
107
+
108
+ @topics_configs[topic][config_hash] = config
109
+ @topics_refs_map[topic][config_hash] = topic_handle
110
+ end
111
+ end
112
+
57
113
  # Starts the native Kafka polling thread and kicks off the init polling
58
114
  # @note Not needed to run unless explicit start was disabled
59
115
  def start
@@ -151,7 +207,18 @@ module Rdkafka
151
207
  def close
152
208
  return if closed?
153
209
  ObjectSpace.undefine_finalizer(self)
154
- @native_kafka.close
210
+
211
+ @native_kafka.close do
212
+ # We need to remove the topics references objects before we destroy the producer,
213
+ # otherwise they would leak out
214
+ @topics_refs_map.each_value do |refs|
215
+ refs.each_value do |ref|
216
+ Rdkafka::Bindings.rd_kafka_topic_destroy(ref)
217
+ end
218
+ end
219
+ end
220
+
221
+ @topics_refs_map.clear
155
222
  end
156
223
 
157
224
  # Whether this producer has closed
@@ -244,11 +311,22 @@ module Rdkafka
244
311
  # @param timestamp [Time,Integer,nil] Optional timestamp of this message. Integer timestamp is in milliseconds since Jan 1 1970.
245
312
  # @param headers [Hash<String,String>] Optional message headers
246
313
  # @param label [Object, nil] a label that can be assigned when producing a message that will be part of the delivery handle and the delivery report
314
+ # @param topic_config [Hash] topic config for given message dispatch. Allows to send messages to topics with different configuration
247
315
  #
248
316
  # @return [DeliveryHandle] Delivery handle that can be used to wait for the result of producing this message
249
317
  #
250
318
  # @raise [RdkafkaError] When adding the message to rdkafka's queue failed
251
- def produce(topic:, payload: nil, key: nil, partition: nil, partition_key: nil, timestamp: nil, headers: nil, label: nil)
319
+ def produce(
320
+ topic:,
321
+ payload: nil,
322
+ key: nil,
323
+ partition: nil,
324
+ partition_key: nil,
325
+ timestamp: nil,
326
+ headers: nil,
327
+ label: nil,
328
+ topic_config: EMPTY_HASH
329
+ )
252
330
  closed_producer_check(__method__)
253
331
 
254
332
  # Start by checking and converting the input
@@ -267,8 +345,20 @@ module Rdkafka
267
345
  key.bytesize
268
346
  end
269
347
 
348
+ topic_config_hash = topic_config.hash
349
+
350
+ # Checks if we have the rdkafka topic reference object ready. It saves us on object
351
+ # allocation and allows to use custom config on demand.
352
+ set_topic_config(topic, topic_config, topic_config_hash) unless @topics_refs_map.dig(topic, topic_config_hash)
353
+ topic_ref = @topics_refs_map.dig(topic, topic_config_hash)
354
+
270
355
  if partition_key
271
356
  partition_count = partition_count(topic)
357
+
358
+ # Check if there are no overrides for the partitioner and use the default one only when
359
+ # no per-topic is present.
360
+ partitioner_name = @topics_configs.dig(topic, topic_config_hash, :partitioner) || @partitioner_name
361
+
272
362
  # If the topic is not present, set to -1
273
363
  partition = Rdkafka::Bindings.partitioner(partition_key, partition_count, @partitioner_name) if partition_count.positive?
274
364
  end
@@ -298,7 +388,7 @@ module Rdkafka
298
388
  DeliveryHandle.register(delivery_handle)
299
389
 
300
390
  args = [
301
- :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_TOPIC, :string, topic,
391
+ :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_RKT, :pointer, topic_ref,
302
392
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_MSGFLAGS, :int, Rdkafka::Bindings::RD_KAFKA_MSG_F_COPY,
303
393
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_VALUE, :buffer_in, payload, :size_t, payload_size,
304
394
  :int, Rdkafka::Bindings::RD_KAFKA_VTYPE_KEY, :buffer_in, key, :size_t, key_size,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdkafka
4
- VERSION = "0.15.0"
4
+ VERSION = "0.15.1"
5
5
  LIBRDKAFKA_VERSION = "2.3.0"
6
6
  LIBRDKAFKA_SOURCE_SHA256 = "2d49c35c77eeb3d42fa61c43757fcbb6a206daa560247154e60642bcdcc14d12"
7
7
  end
@@ -31,6 +31,14 @@ describe Rdkafka::Admin do
31
31
  let(:operation) {Rdkafka::Bindings::RD_KAFKA_ACL_OPERATION_READ}
32
32
  let(:permission_type) {Rdkafka::Bindings::RD_KAFKA_ACL_PERMISSION_TYPE_ALLOW}
33
33
 
34
+ describe '#describe_errors' do
35
+ let(:errors) { admin.class.describe_errors }
36
+
37
+ it { expect(errors.size).to eq(162) }
38
+ it { expect(errors[-184]).to eq(code: -184, description: 'Local: Queue full', name: '_QUEUE_FULL') }
39
+ it { expect(errors[21]).to eq(code: 21, description: 'Broker: Invalid required acks value', name: 'INVALID_REQUIRED_ACKS') }
40
+ end
41
+
34
42
  describe 'admin without auto-start' do
35
43
  let(:admin) { config.admin(native_kafka_auto_start: false) }
36
44
 
@@ -31,6 +31,48 @@ describe Rdkafka::Producer do
31
31
  it { expect(producer.name).to include('rdkafka#producer-') }
32
32
  end
33
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" => "localhost: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
+
34
76
  context "delivery callback" do
35
77
  context "with a proc/lambda" do
36
78
  it "should set the callback" do
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: karafka-rdkafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thijs Cadier
@@ -36,7 +36,7 @@ cert_chain:
36
36
  AnG1dJU+yL2BK7vaVytLTstJME5mepSZ46qqIJXMuWob/YPDmVaBF39TDSG9e34s
37
37
  msG3BiCqgOgHAnL23+CN3Rt8MsuRfEtoTKpJVcCfoEoNHOkc
38
38
  -----END CERTIFICATE-----
39
- date: 2024-04-26 00:00:00.000000000 Z
39
+ date: 2024-05-09 00:00:00.000000000 Z
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: ffi
metadata.gz.sig CHANGED
Binary file