karafka-rdkafka 0.23.0.beta2-arm64-darwin → 0.23.1.rc1-arm64-darwin

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: 9f502d8686bf1b5327ce301418e6fe207318cda982e3b6024f96e4b67564994a
4
- data.tar.gz: 257800302b2ea158b27be09ff202998b7938947451df24c4a354bd7e529472eb
3
+ metadata.gz: 6fa37f05662fa3ac3af06c2efac7b3253a0a0216a6dea7cf67d3541c6b785e2b
4
+ data.tar.gz: 4e525c0932641e87bad94d96e370d2530a5aba1ff49bf33faf3b76b87aca3653
5
5
  SHA512:
6
- metadata.gz: 3583ac8e7cdf5cd871a1152c34d12395ccd9e4b405fb994f0c9180b69fc9e2a02ba6792bfcf8d812f5ec4535b5a013f4a390a8e33ba63f91fd6dc1abe302f86c
7
- data.tar.gz: f5380cefddaad754e6560ca27ef26cde84fafeecda1fe89c9cfb8ea532e6576435068cfa9043b501afb71de948a6b694851b2945156d357832f96db71bab2bf7
6
+ metadata.gz: 811cd4e18589fe4836f45c8c707dc5065905c5dd24d7844ecbcb425ffda3ab6f1ab5e4a4ac7578b7420a792224924692a5ca01b370ad7bff7db1ade8e5e697f2
7
+ data.tar.gz: 4068d9eca642fb548e8e8e85e4839210bc8ce4a5616dbfd346b514c78092e7aab10928cbec103871ee64f2c66ec2bf18f129287313819ac0bb0bfcc979b861f1
data/CHANGELOG.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # Rdkafka Changelog
2
2
 
3
- ## 0.23.0 (Unreleased)
3
+ ## 0.23.1 (Unreleased)
4
+ - **[Feature]** Add integrated fatal error handling in `RdkafkaError.validate!` - automatically detects and handles fatal errors (-150) with single entrypoint API.
5
+ - [Enhancement] Add optional `client_ptr` parameter to `validate!` for automatic fatal error remapping to actual underlying error codes.
6
+ - [Enhancement] Update all Producer and Consumer `validate!` calls to provide `client_ptr` for comprehensive fatal error handling.
7
+ - [Enhancement] Add `rd_kafka_fatal_error()` FFI binding to retrieve actual fatal error details.
8
+ - [Enhancement] Add `rd_kafka_test_fatal_error()` FFI binding for testing fatal error scenarios.
9
+ - [Enhancement] Add `RdkafkaError.build_fatal` class method for centralized fatal error construction.
10
+ - [Enhancement] Add comprehensive tests for fatal error handling including unit tests and integration tests.
11
+ - [Enhancement] Add `RD_KAFKA_PARTITION_UA` constant for unassigned partition (-1).
12
+ - [Enhancement] Replace magic numbers with named constants: use `RD_KAFKA_RESP_ERR_NO_ERROR` instead of `0` for error code checks (18 instances) and `RD_KAFKA_PARTITION_UA` instead of `-1` for partition values (9 instances) across the codebase for better code clarity and maintainability.
13
+ - [Enhancement] Add `Rdkafka::Testing` module for testing fatal error scenarios on both producers and consumers.
14
+ - [Deprecated] `RdkafkaError.validate_fatal!` - use `validate!` with `client_ptr` parameter instead.
15
+
16
+ ## 0.23.0 (2025-11-01)
4
17
  - [Enhancement] Bump librdkafka to 2.12.1.
5
18
  - [Enhancement] Force lock FFI to 1.17.1 or higher to include critical bug fixes around GCC, write barriers, and thread restarts for forks.
6
19
  - [Fix] Fix for Core dump when providing extensions to oauthbearer_set_token (dssjoblom)
data/README.md CHANGED
@@ -63,7 +63,7 @@ Contributions should generally be made to the upstream [rdkafka-ruby repository]
63
63
 
64
64
  | rdkafka-ruby | librdkafka | patches |
65
65
  |-|-|-|
66
- | 0.23.x (Unreleased) | 2.12.1 (2025-10-16) | yes |
66
+ | 0.23.x (2025-11-01) | 2.12.1 (2025-10-16) | yes |
67
67
  | 0.22.x (2025-09-26) | 2.11.1 (2025-08-18) | yes |
68
68
  | 0.21.x (2025-08-18) | 2.11.0 (2025-07-03) | yes |
69
69
  | 0.20.x (2025-07-17) | 2.8.0 (2025-01-07) | yes |
data/ext/librdkafka.dylib CHANGED
Binary file
@@ -77,7 +77,7 @@ module Rdkafka
77
77
  "Waiting for #{operation_name} timed out after #{max_wait_timeout} seconds"
78
78
  )
79
79
  end
80
- elsif self[:response] != 0 && raise_response_error
80
+ elsif self[:response] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR && raise_response_error
81
81
  raise_error
82
82
  else
83
83
  return create_result
data/lib/rdkafka/admin.rb CHANGED
@@ -327,7 +327,7 @@ module Rdkafka
327
327
  # Create and register the handle we will return to the caller
328
328
  create_partitions_handle = CreatePartitionsHandle.new
329
329
  create_partitions_handle[:pending] = true
330
- create_partitions_handle[:response] = -1
330
+ create_partitions_handle[:response] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
331
331
  CreatePartitionsHandle.register(create_partitions_handle)
332
332
  admin_options_ptr = Rdkafka::Bindings.rd_kafka_AdminOptions_new(inner, Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_CREATEPARTITIONS)
333
333
  Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, create_partitions_handle.to_ptr)
@@ -28,8 +28,17 @@ module Rdkafka
28
28
  RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS = -174
29
29
  RD_KAFKA_RESP_ERR__STATE = -172
30
30
  RD_KAFKA_RESP_ERR__NOENT = -156
31
+ RD_KAFKA_RESP_ERR__FATAL = -150
31
32
  RD_KAFKA_RESP_ERR_NO_ERROR = 0
32
33
 
34
+ # Buffer size for fatal error strings, matches librdkafka expectations
35
+ FATAL_ERROR_BUFFER_SIZE = 256
36
+
37
+ # Unassigned partition
38
+ RD_KAFKA_PARTITION_UA = -1
39
+ # String representation of unassigned partition (used in stats hash keys)
40
+ RD_KAFKA_PARTITION_UA_STR = '-1'.freeze
41
+
33
42
  RD_KAFKA_OFFSET_END = -1
34
43
  RD_KAFKA_OFFSET_BEGINNING = -2
35
44
  RD_KAFKA_OFFSET_STORED = -1000
@@ -161,6 +170,8 @@ module Rdkafka
161
170
  attach_function :rd_kafka_error_txn_requires_abort, [:pointer], :int
162
171
  attach_function :rd_kafka_error_destroy, [:pointer], :void
163
172
  attach_function :rd_kafka_get_err_descs, [:pointer, :pointer], :void
173
+ attach_function :rd_kafka_fatal_error, [:pointer, :pointer, :int], :int
174
+ attach_function :rd_kafka_test_fatal_error, [:pointer, :int, :string], :int
164
175
 
165
176
  # Configuration
166
177
 
@@ -234,7 +245,7 @@ module Rdkafka
234
245
  # Since this cache is shared, having few consumers and/or producers in one process will
235
246
  # automatically improve the querying times even with low refresh times.
236
247
  (stats['topics'] || EMPTY_HASH).each do |topic_name, details|
237
- partitions_count = details['partitions'].keys.reject { |k| k == '-1' }.size
248
+ partitions_count = details['partitions'].keys.reject { |k| k == RD_KAFKA_PARTITION_UA_STR }.size
238
249
 
239
250
  next unless partitions_count.positive?
240
251
 
@@ -248,11 +259,48 @@ module Rdkafka
248
259
  0
249
260
  end
250
261
 
262
+ # Retrieves fatal error details from a kafka client handle.
263
+ # This is a helper method to extract fatal error information consistently
264
+ # across different parts of the codebase (callbacks, testing utilities, etc.).
265
+ #
266
+ # @param client_ptr [FFI::Pointer] Native kafka client pointer
267
+ # @return [Hash, nil] Hash with :error_code and :error_string if fatal error occurred, nil otherwise
268
+ #
269
+ # @example
270
+ # details = Rdkafka::Bindings.extract_fatal_error(client_ptr)
271
+ # if details
272
+ # puts "Fatal error #{details[:error_code]}: #{details[:error_string]}"
273
+ # end
274
+ def self.extract_fatal_error(client_ptr)
275
+ error_buffer = FFI::MemoryPointer.new(:char, FATAL_ERROR_BUFFER_SIZE)
276
+
277
+ error_code = rd_kafka_fatal_error(client_ptr, error_buffer, FATAL_ERROR_BUFFER_SIZE)
278
+
279
+ return nil if error_code == RD_KAFKA_RESP_ERR_NO_ERROR
280
+
281
+ {
282
+ error_code: error_code,
283
+ error_string: error_buffer.read_string
284
+ }
285
+ end
286
+
251
287
  ErrorCallback = FFI::Function.new(
252
288
  :void, [:pointer, :int, :string, :pointer]
253
- ) do |_client_prr, err_code, reason, _opaque|
289
+ ) do |client_ptr, err_code, reason, _opaque|
254
290
  if Rdkafka::Config.error_callback
255
- error = Rdkafka::RdkafkaError.build(err_code, broker_message: reason)
291
+ # Handle fatal errors according to librdkafka documentation:
292
+ # When ERR__FATAL is received, we must call rd_kafka_fatal_error()
293
+ # to get the actual underlying fatal error code and description.
294
+ if err_code == RD_KAFKA_RESP_ERR__FATAL
295
+ error = Rdkafka::RdkafkaError.build_fatal(
296
+ client_ptr,
297
+ fallback_error_code: err_code,
298
+ fallback_message: reason
299
+ )
300
+ else
301
+ error = Rdkafka::RdkafkaError.build(err_code, broker_message: reason)
302
+ end
303
+
256
304
  error.set_backtrace(caller)
257
305
  Rdkafka::Config.error_callback.call(error)
258
306
  end
@@ -404,7 +452,7 @@ module Rdkafka
404
452
 
405
453
  def self.partitioner(topic_ptr, str, partition_count, partitioner = "consistent_random")
406
454
  # Return RD_KAFKA_PARTITION_UA(unassigned partition) when partition count is nil/zero.
407
- return -1 unless partition_count&.nonzero?
455
+ return RD_KAFKA_PARTITION_UA unless partition_count&.nonzero?
408
456
 
409
457
  str_ptr = str.empty? ? FFI::MemoryPointer::NULL : FFI::MemoryPointer.from_string(str)
410
458
  method_name = PARTITIONERS.fetch(partitioner) do
@@ -76,7 +76,7 @@ module Rdkafka
76
76
  rd_kafka_error_pointer = Rdkafka::Bindings.rd_kafka_DeleteAcls_result_response_error(acl_result_pointer)
77
77
  @result_error = Rdkafka::Bindings.rd_kafka_error_code(rd_kafka_error_pointer)
78
78
  @error_string = Rdkafka::Bindings.rd_kafka_error_string(rd_kafka_error_pointer)
79
- if @result_error == 0
79
+ if @result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
80
80
  # Get the number of matching acls
81
81
  pointer_to_size_t = FFI::MemoryPointer.new(:int32)
82
82
  @matching_acls = Rdkafka::Bindings.rd_kafka_DeleteAcls_result_response_matching_acls(acl_result_pointer, pointer_to_size_t)
@@ -102,7 +102,7 @@ module Rdkafka
102
102
  @matching_acls=[]
103
103
  @result_error = Rdkafka::Bindings.rd_kafka_event_error(event_ptr)
104
104
  @error_string = Rdkafka::Bindings.rd_kafka_event_error_string(event_ptr)
105
- if @result_error == 0
105
+ if @result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
106
106
  acl_describe_result = Rdkafka::Bindings.rd_kafka_event_DescribeAcls_result(event_ptr)
107
107
  # Get the number of matching acls
108
108
  pointer_to_size_t = FFI::MemoryPointer.new(:int32)
@@ -120,7 +120,7 @@ module Rdkafka
120
120
  @result_error = Rdkafka::Bindings.rd_kafka_event_error(event_ptr)
121
121
  @error_string = Rdkafka::Bindings.rd_kafka_event_error_string(event_ptr)
122
122
 
123
- if @result_error == 0
123
+ if @result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
124
124
  configs_describe_result = Rdkafka::Bindings.rd_kafka_event_DescribeConfigs_result(event_ptr)
125
125
  # Get the number of matching acls
126
126
  pointer_to_size_t = FFI::MemoryPointer.new(:int32)
@@ -138,7 +138,7 @@ module Rdkafka
138
138
  @result_error = Rdkafka::Bindings.rd_kafka_event_error(event_ptr)
139
139
  @error_string = Rdkafka::Bindings.rd_kafka_event_error_string(event_ptr)
140
140
 
141
- if @result_error == 0
141
+ if @result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
142
142
  incremental_alter_result = Rdkafka::Bindings.rd_kafka_event_IncrementalAlterConfigs_result(event_ptr)
143
143
  # Get the number of matching acls
144
144
  pointer_to_size_t = FFI::MemoryPointer.new(:int32)
@@ -202,7 +202,7 @@ module Rdkafka
202
202
  describe_configs_handle[:response_string] = describe_configs.error_string
203
203
  describe_configs_handle[:pending] = false
204
204
 
205
- if describe_configs.result_error == 0
205
+ if describe_configs.result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
206
206
  describe_configs_handle[:config_entries] = describe_configs.results
207
207
  describe_configs_handle[:entry_count] = describe_configs.results_count
208
208
  end
@@ -220,7 +220,7 @@ module Rdkafka
220
220
  incremental_alter_handle[:response_string] = incremental_alter.error_string
221
221
  incremental_alter_handle[:pending] = false
222
222
 
223
- if incremental_alter.result_error == 0
223
+ if incremental_alter.result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
224
224
  incremental_alter_handle[:config_entries] = incremental_alter.results
225
225
  incremental_alter_handle[:entry_count] = incremental_alter.results_count
226
226
  end
@@ -313,7 +313,7 @@ module Rdkafka
313
313
  delete_acl_handle[:response] = delete_acl_results[0].result_error
314
314
  delete_acl_handle[:response_string] = delete_acl_results[0].error_string
315
315
 
316
- if delete_acl_results[0].result_error == 0
316
+ if delete_acl_results[0].result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
317
317
  delete_acl_handle[:matching_acls] = delete_acl_results[0].matching_acls
318
318
  delete_acl_handle[:matching_acls_count] = delete_acl_results[0].matching_acls_count
319
319
  end
@@ -330,7 +330,7 @@ module Rdkafka
330
330
  describe_acl_handle[:response] = describe_acl.result_error
331
331
  describe_acl_handle[:response_string] = describe_acl.error_string
332
332
 
333
- if describe_acl.result_error == 0
333
+ if describe_acl.result_error == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
334
334
  describe_acl_handle[:acls] = describe_acl.matching_acls
335
335
  describe_acl_handle[:acls_count] = describe_acl.matching_acls_count
336
336
  end
@@ -33,7 +33,7 @@ module Rdkafka
33
33
  def to_s
34
34
  message = "<Partition #{partition}"
35
35
  message += " offset=#{offset}" if offset
36
- message += " err=#{err}" if err != 0
36
+ message += " err=#{err}" if err != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
37
37
  message += " metadata=#{metadata}" if metadata != nil
38
38
  message += ">"
39
39
  message
@@ -109,7 +109,7 @@ module Rdkafka
109
109
  native_tpl[:cnt].times do |i|
110
110
  ptr = native_tpl[:elems] + (i * Rdkafka::Bindings::TopicPartition.size)
111
111
  elem = Rdkafka::Bindings::TopicPartition.new(ptr)
112
- if elem[:partition] == -1
112
+ if elem[:partition] == Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
113
113
  data[elem[:topic]] = nil
114
114
  else
115
115
  partitions = data[elem[:topic]] || []
@@ -67,15 +67,14 @@ module Rdkafka
67
67
  tpl = Rdkafka::Bindings.rd_kafka_topic_partition_list_new(topics.length)
68
68
 
69
69
  topics.each do |topic|
70
- Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, -1)
70
+ Rdkafka::Bindings.rd_kafka_topic_partition_list_add(tpl, topic, Rdkafka::Bindings::RD_KAFKA_PARTITION_UA)
71
71
  end
72
72
 
73
73
  # Subscribe to topic partition list and check this was successful
74
- response = @native_kafka.with_inner do |inner|
75
- Rdkafka::Bindings.rd_kafka_subscribe(inner, tpl)
74
+ @native_kafka.with_inner do |inner|
75
+ response = Rdkafka::Bindings.rd_kafka_subscribe(inner, tpl)
76
+ Rdkafka::RdkafkaError.validate!(response, "Error subscribing to '#{topics.join(', ')}'", client_ptr: inner)
76
77
  end
77
-
78
- Rdkafka::RdkafkaError.validate!(response, "Error subscribing to '#{topics.join(', ')}'")
79
78
  ensure
80
79
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) unless tpl.nil?
81
80
  end
@@ -87,12 +86,11 @@ module Rdkafka
87
86
  def unsubscribe
88
87
  closed_consumer_check(__method__)
89
88
 
90
- response = @native_kafka.with_inner do |inner|
91
- Rdkafka::Bindings.rd_kafka_unsubscribe(inner)
89
+ @native_kafka.with_inner do |inner|
90
+ response = Rdkafka::Bindings.rd_kafka_unsubscribe(inner)
91
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
92
92
  end
93
93
 
94
- Rdkafka::RdkafkaError.validate!(response)
95
-
96
94
  nil
97
95
  end
98
96
 
@@ -115,7 +113,7 @@ module Rdkafka
115
113
  Rdkafka::Bindings.rd_kafka_pause_partitions(inner, tpl)
116
114
  end
117
115
 
118
- if response != 0
116
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
119
117
  list = TopicPartitionList.from_native_tpl(tpl)
120
118
  raise Rdkafka::RdkafkaTopicPartitionListError.new(response, list, "Error pausing '#{list.to_h}'")
121
119
  end
@@ -139,12 +137,11 @@ module Rdkafka
139
137
  tpl = list.to_native_tpl
140
138
 
141
139
  begin
142
- response = @native_kafka.with_inner do |inner|
143
- Rdkafka::Bindings.rd_kafka_resume_partitions(inner, tpl)
140
+ @native_kafka.with_inner do |inner|
141
+ response = Rdkafka::Bindings.rd_kafka_resume_partitions(inner, tpl)
142
+ Rdkafka::RdkafkaError.validate!(response, "Error resume '#{list.to_h}'", client_ptr: inner)
144
143
  end
145
144
 
146
- Rdkafka::RdkafkaError.validate!(response, "Error resume '#{list.to_h}'")
147
-
148
145
  nil
149
146
  ensure
150
147
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
@@ -159,12 +156,11 @@ module Rdkafka
159
156
  closed_consumer_check(__method__)
160
157
 
161
158
  ptr = FFI::MemoryPointer.new(:pointer)
162
- response = @native_kafka.with_inner do |inner|
163
- Rdkafka::Bindings.rd_kafka_subscription(inner, ptr)
159
+ @native_kafka.with_inner do |inner|
160
+ response = Rdkafka::Bindings.rd_kafka_subscription(inner, ptr)
161
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
164
162
  end
165
163
 
166
- Rdkafka::RdkafkaError.validate!(response)
167
-
168
164
  native = ptr.read_pointer
169
165
 
170
166
  begin
@@ -188,11 +184,10 @@ module Rdkafka
188
184
  tpl = list.to_native_tpl
189
185
 
190
186
  begin
191
- response = @native_kafka.with_inner do |inner|
192
- Rdkafka::Bindings.rd_kafka_assign(inner, tpl)
187
+ @native_kafka.with_inner do |inner|
188
+ response = Rdkafka::Bindings.rd_kafka_assign(inner, tpl)
189
+ Rdkafka::RdkafkaError.validate!(response, "Error assigning '#{list.to_h}'", client_ptr: inner)
193
190
  end
194
-
195
- Rdkafka::RdkafkaError.validate!(response, "Error assigning '#{list.to_h}'")
196
191
  ensure
197
192
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
198
193
  end
@@ -206,12 +201,11 @@ module Rdkafka
206
201
  closed_consumer_check(__method__)
207
202
 
208
203
  ptr = FFI::MemoryPointer.new(:pointer)
209
- response = @native_kafka.with_inner do |inner|
210
- Rdkafka::Bindings.rd_kafka_assignment(inner, ptr)
204
+ @native_kafka.with_inner do |inner|
205
+ response = Rdkafka::Bindings.rd_kafka_assignment(inner, ptr)
206
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
211
207
  end
212
208
 
213
- Rdkafka::RdkafkaError.validate!(response)
214
-
215
209
  tpl = ptr.read_pointer
216
210
 
217
211
  if !tpl.null?
@@ -255,12 +249,11 @@ module Rdkafka
255
249
  tpl = list.to_native_tpl
256
250
 
257
251
  begin
258
- response = @native_kafka.with_inner do |inner|
259
- Rdkafka::Bindings.rd_kafka_committed(inner, tpl, timeout_ms)
252
+ @native_kafka.with_inner do |inner|
253
+ response = Rdkafka::Bindings.rd_kafka_committed(inner, tpl, timeout_ms)
254
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
260
255
  end
261
256
 
262
- Rdkafka::RdkafkaError.validate!(response)
263
-
264
257
  TopicPartitionList.from_native_tpl(tpl)
265
258
  ensure
266
259
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl)
@@ -284,12 +277,11 @@ module Rdkafka
284
277
 
285
278
  tpl = list.to_native_tpl
286
279
 
287
- response = @native_kafka.with_inner do |inner|
288
- Rdkafka::Bindings.rd_kafka_position(inner, tpl)
280
+ @native_kafka.with_inner do |inner|
281
+ response = Rdkafka::Bindings.rd_kafka_position(inner, tpl)
282
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
289
283
  end
290
284
 
291
- Rdkafka::RdkafkaError.validate!(response)
292
-
293
285
  TopicPartitionList.from_native_tpl(tpl)
294
286
  end
295
287
 
@@ -306,8 +298,8 @@ module Rdkafka
306
298
  low = FFI::MemoryPointer.new(:int64, 1)
307
299
  high = FFI::MemoryPointer.new(:int64, 1)
308
300
 
309
- response = @native_kafka.with_inner do |inner|
310
- Rdkafka::Bindings.rd_kafka_query_watermark_offsets(
301
+ @native_kafka.with_inner do |inner|
302
+ response = Rdkafka::Bindings.rd_kafka_query_watermark_offsets(
311
303
  inner,
312
304
  topic,
313
305
  partition,
@@ -315,10 +307,9 @@ module Rdkafka
315
307
  high,
316
308
  timeout_ms,
317
309
  )
310
+ Rdkafka::RdkafkaError.validate!(response, "Error querying watermark offsets for partition #{partition} of #{topic}", client_ptr: inner)
318
311
  end
319
312
 
320
- Rdkafka::RdkafkaError.validate!(response, "Error querying watermark offsets for partition #{partition} of #{topic}")
321
-
322
313
  return low.read_array_of_int64(1).first, high.read_array_of_int64(1).first
323
314
  ensure
324
315
  low.free unless low.nil?
@@ -414,15 +405,14 @@ module Rdkafka
414
405
 
415
406
  tpl = list.to_native_tpl
416
407
 
417
- response = @native_kafka.with_inner do |inner|
418
- Rdkafka::Bindings.rd_kafka_offsets_store(
408
+ @native_kafka.with_inner do |inner|
409
+ response = Rdkafka::Bindings.rd_kafka_offsets_store(
419
410
  inner,
420
411
  tpl
421
412
  )
413
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
422
414
  end
423
415
 
424
- Rdkafka::RdkafkaError.validate!(response)
425
-
426
416
  nil
427
417
  ensure
428
418
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
@@ -459,15 +449,19 @@ module Rdkafka
459
449
  nil
460
450
  )
461
451
  end
452
+
462
453
  response = Rdkafka::Bindings.rd_kafka_seek(
463
454
  native_topic,
464
455
  partition,
465
456
  offset,
466
457
  0 # timeout
467
458
  )
468
- Rdkafka::RdkafkaError.validate!(response)
469
459
 
470
- nil
460
+ return nil if response == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
461
+
462
+ @native_kafka.with_inner do |inner|
463
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
464
+ end
471
465
  ensure
472
466
  if native_topic && !native_topic.null?
473
467
  Rdkafka::Bindings.rd_kafka_topic_destroy(native_topic)
@@ -490,16 +484,15 @@ module Rdkafka
490
484
 
491
485
  tpl = list.to_native_tpl
492
486
 
493
- response = @native_kafka.with_inner do |inner|
494
- Rdkafka::Bindings.rd_kafka_offsets_for_times(
487
+ @native_kafka.with_inner do |inner|
488
+ response = Rdkafka::Bindings.rd_kafka_offsets_for_times(
495
489
  inner,
496
490
  tpl,
497
491
  timeout_ms # timeout
498
492
  )
493
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
499
494
  end
500
495
 
501
- Rdkafka::RdkafkaError.validate!(response)
502
-
503
496
  TopicPartitionList.from_native_tpl(tpl)
504
497
  ensure
505
498
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
@@ -528,12 +521,11 @@ module Rdkafka
528
521
  tpl = list ? list.to_native_tpl : nil
529
522
 
530
523
  begin
531
- response = @native_kafka.with_inner do |inner|
532
- Rdkafka::Bindings.rd_kafka_commit(inner, tpl, async)
524
+ @native_kafka.with_inner do |inner|
525
+ response = Rdkafka::Bindings.rd_kafka_commit(inner, tpl, async)
526
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
533
527
  end
534
528
 
535
- Rdkafka::RdkafkaError.validate!(response)
536
-
537
529
  nil
538
530
  ensure
539
531
  Rdkafka::Bindings.rd_kafka_topic_partition_list_destroy(tpl) if tpl
@@ -558,10 +550,12 @@ module Rdkafka
558
550
  native_message = Rdkafka::Bindings::Message.new(message_ptr)
559
551
 
560
552
  # Create a message to pass out
561
- return Rdkafka::Consumer::Message.new(native_message) if native_message[:err].zero?
553
+ return Rdkafka::Consumer::Message.new(native_message) if native_message[:err] == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
562
554
 
563
- # Raise error if needed
564
- Rdkafka::RdkafkaError.validate!(native_message)
555
+ # Raise error if needed - wrap just the validate! call with client access
556
+ @native_kafka.with_inner do |inner|
557
+ Rdkafka::RdkafkaError.validate!(native_message, client_ptr: inner)
558
+ end
565
559
  ensure
566
560
  # Clean up rdkafka message if there is one
567
561
  if message_ptr && !message_ptr.null?
data/lib/rdkafka/error.rb CHANGED
@@ -29,7 +29,7 @@ module Rdkafka
29
29
  def build_from_c(response_ptr, message_prefix = nil, broker_message: nil)
30
30
  code = Rdkafka::Bindings.rd_kafka_error_code(response_ptr)
31
31
 
32
- return false if code.zero?
32
+ return false if code == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
33
33
 
34
34
  message = broker_message || Rdkafka::Bindings.rd_kafka_err2str(code)
35
35
  fatal = !Rdkafka::Bindings.rd_kafka_error_is_fatal(response_ptr).zero?
@@ -51,11 +51,11 @@ module Rdkafka
51
51
  def build(response_ptr_or_code, message_prefix = nil, broker_message: nil)
52
52
  case response_ptr_or_code
53
53
  when Integer
54
- return false if response_ptr_or_code.zero?
54
+ return false if response_ptr_or_code == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
55
55
 
56
56
  new(response_ptr_or_code, message_prefix, broker_message: broker_message)
57
57
  when Bindings::Message
58
- return false if response_ptr_or_code[:err].zero?
58
+ return false if response_ptr_or_code[:err] == Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
59
59
 
60
60
  unless response_ptr_or_code[:payload].null?
61
61
  message_prefix ||= response_ptr_or_code[:payload].read_string(response_ptr_or_code[:len])
@@ -81,9 +81,49 @@ module Rdkafka
81
81
  end
82
82
  end
83
83
 
84
- def validate!(response_ptr_or_code, message_prefix = nil, broker_message: nil)
84
+ def validate!(response_ptr_or_code, message_prefix = nil, broker_message: nil, client_ptr: nil)
85
85
  error = build(response_ptr_or_code, message_prefix, broker_message: broker_message)
86
- error ? raise(error) : false
86
+
87
+ return false unless error
88
+
89
+ # Auto-detect and handle fatal errors (-150)
90
+ if error.rdkafka_response == Bindings::RD_KAFKA_RESP_ERR__FATAL && client_ptr
91
+ # Discover the underlying fatal error from librdkafka
92
+ error = build_fatal(
93
+ client_ptr,
94
+ fallback_error_code: error.rdkafka_response,
95
+ fallback_message: broker_message
96
+ )
97
+ end
98
+
99
+ raise error
100
+ end
101
+
102
+ # Build a fatal error from librdkafka's fatal error state.
103
+ # Calls rd_kafka_fatal_error() to get the actual underlying error code and description.
104
+ #
105
+ # @param client_ptr [FFI::Pointer] Pointer to rd_kafka_t client
106
+ # @param fallback_error_code [Integer] Error code to use if no fatal error found (default: -150)
107
+ # @param fallback_message [String, nil] Message to use if no fatal error found
108
+ # @return [RdkafkaError] Error object with fatal flag set to true
109
+ def build_fatal(client_ptr, fallback_error_code: -150, fallback_message: nil)
110
+ fatal_error_details = Rdkafka::Bindings.extract_fatal_error(client_ptr)
111
+
112
+ if fatal_error_details
113
+ new(
114
+ fatal_error_details[:error_code],
115
+ broker_message: fatal_error_details[:error_string],
116
+ fatal: true
117
+ )
118
+ else
119
+ # Fallback: if extract_fatal_error returns nil (shouldn't happen in practice),
120
+ # the error code itself still indicates a fatal condition
121
+ new(
122
+ fallback_error_code,
123
+ broker_message: fallback_message,
124
+ fatal: true
125
+ )
126
+ end
87
127
  end
88
128
  end
89
129
 
@@ -26,7 +26,7 @@ module Rdkafka
26
26
  extensions_ptr&.free
27
27
  end
28
28
 
29
- return response if response.zero?
29
+ return response if response == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
30
30
 
31
31
  oauthbearer_set_token_failure("Failed to set token: #{error_buffer.read_string}")
32
32
 
@@ -25,7 +25,7 @@ module Rdkafka
25
25
 
26
26
  # @return [DeliveryReport] a report on the delivery of the message
27
27
  def create_result
28
- if self[:response] == 0
28
+ if self[:response] == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
29
29
  DeliveryReport.new(
30
30
  self[:partition],
31
31
  self[:offset],
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rdkafka
4
+ # Testing utilities for Producer instances.
5
+ # This module is NOT included by default and should only be used in test environments.
6
+ #
7
+ # This module provides librdkafka native testing utilities that are needed to trigger certain
8
+ # behaviours that are hard to reproduce in stable environments, particularly fatal error
9
+ # scenarios in idempotent and transactional producers.
10
+ #
11
+ # To use in tests for producers:
12
+ # producer.singleton_class.include(Rdkafka::Testing)
13
+ #
14
+ # Or include it for all producers in your test suite:
15
+ # Rdkafka::Producer.include(Rdkafka::Testing)
16
+ #
17
+ # IMPORTANT: Fatal errors leave the producer client in an unusable state. After triggering
18
+ # a fatal error, the producer should be closed and discarded. Do not attempt to reuse a
19
+ # producer that has experienced a fatal error.
20
+ module Testing
21
+ # Triggers a test fatal error using rd_kafka_test_fatal_error.
22
+ # This is useful for testing fatal error handling without needing actual broker issues.
23
+ #
24
+ # @param error_code [Integer] The error code to trigger (e.g., 47 for invalid_producer_epoch)
25
+ # @param reason [String] Descriptive reason for the error
26
+ # @return [Integer] Result code from rd_kafka_test_fatal_error (0 on success)
27
+ #
28
+ # @example
29
+ # producer.trigger_test_fatal_error(47, "Test producer fencing")
30
+ def trigger_test_fatal_error(error_code, reason)
31
+ @native_kafka.with_inner do |inner|
32
+ Rdkafka::Bindings.rd_kafka_test_fatal_error(inner, error_code, reason)
33
+ end
34
+ end
35
+
36
+ # Checks if a fatal error has occurred and retrieves error details.
37
+ # Calls rd_kafka_fatal_error to get the actual fatal error code and message.
38
+ #
39
+ # @return [Hash, nil] Hash with :error_code and :error_string if fatal error occurred, nil otherwise
40
+ #
41
+ # @example
42
+ # if fatal_error = producer.fatal_error
43
+ # puts "Fatal error #{fatal_error[:error_code]}: #{fatal_error[:error_string]}"
44
+ # end
45
+ def fatal_error
46
+ @native_kafka.with_inner do |inner|
47
+ Rdkafka::Bindings.extract_fatal_error(inner)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -142,7 +142,7 @@ module Rdkafka
142
142
  @native_kafka.with_inner do |inner|
143
143
  response_ptr = Rdkafka::Bindings.rd_kafka_init_transactions(inner, -1)
144
144
 
145
- Rdkafka::RdkafkaError.validate!(response_ptr) || true
145
+ Rdkafka::RdkafkaError.validate!(response_ptr, client_ptr: inner) || true
146
146
  end
147
147
  end
148
148
 
@@ -152,7 +152,7 @@ module Rdkafka
152
152
  @native_kafka.with_inner do |inner|
153
153
  response_ptr = Rdkafka::Bindings.rd_kafka_begin_transaction(inner)
154
154
 
155
- Rdkafka::RdkafkaError.validate!(response_ptr) || true
155
+ Rdkafka::RdkafkaError.validate!(response_ptr, client_ptr: inner) || true
156
156
  end
157
157
  end
158
158
 
@@ -162,7 +162,7 @@ module Rdkafka
162
162
  @native_kafka.with_inner do |inner|
163
163
  response_ptr = Rdkafka::Bindings.rd_kafka_commit_transaction(inner, timeout_ms)
164
164
 
165
- Rdkafka::RdkafkaError.validate!(response_ptr) || true
165
+ Rdkafka::RdkafkaError.validate!(response_ptr, client_ptr: inner) || true
166
166
  end
167
167
  end
168
168
 
@@ -171,7 +171,7 @@ module Rdkafka
171
171
 
172
172
  @native_kafka.with_inner do |inner|
173
173
  response_ptr = Rdkafka::Bindings.rd_kafka_abort_transaction(inner, timeout_ms)
174
- Rdkafka::RdkafkaError.validate!(response_ptr) || true
174
+ Rdkafka::RdkafkaError.validate!(response_ptr, client_ptr: inner) || true
175
175
  end
176
176
  end
177
177
 
@@ -192,7 +192,7 @@ module Rdkafka
192
192
  @native_kafka.with_inner do |inner|
193
193
  response_ptr = Bindings.rd_kafka_send_offsets_to_transaction(inner, native_tpl, cgmetadata, timeout_ms)
194
194
 
195
- Rdkafka::RdkafkaError.validate!(response_ptr)
195
+ Rdkafka::RdkafkaError.validate!(response_ptr, client_ptr: inner)
196
196
  end
197
197
  ensure
198
198
  if cgmetadata && !cgmetadata.null?
@@ -266,7 +266,7 @@ module Rdkafka
266
266
  Bindings::RD_KAFKA_PURGE_F_QUEUE | Bindings::RD_KAFKA_PURGE_F_INFLIGHT
267
267
  )
268
268
 
269
- Rdkafka::RdkafkaError.validate!(response)
269
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
270
270
  end
271
271
 
272
272
  # Wait for the purge to affect everything
@@ -278,7 +278,7 @@ module Rdkafka
278
278
  # Partition count for a given topic.
279
279
  #
280
280
  # @param topic [String] The topic name.
281
- # @return [Integer] partition count for a given topic or `-1` if it could not be obtained.
281
+ # @return [Integer] partition count for a given topic or `RD_KAFKA_PARTITION_UA (-1)` if it could not be obtained.
282
282
  #
283
283
  # @note If 'allow.auto.create.topics' is set to true in the broker, the topic will be
284
284
  # auto-created after returning nil.
@@ -299,7 +299,7 @@ module Rdkafka
299
299
  topic_metadata = ::Rdkafka::Metadata.new(inner, topic).topics&.first
300
300
  end
301
301
 
302
- topic_metadata ? topic_metadata[:partition_count] : -1
302
+ topic_metadata ? topic_metadata[:partition_count] : Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
303
303
  end
304
304
  rescue Rdkafka::RdkafkaError => e
305
305
  # If the topic does not exist, it will be created or if not allowed another error will be
@@ -380,9 +380,9 @@ module Rdkafka
380
380
  selected_partitioner) if partition_count.positive?
381
381
  end
382
382
 
383
- # If partition is nil, use -1 to let librdafka set the partition randomly or
383
+ # If partition is nil, use RD_KAFKA_PARTITION_UA to let librdafka set the partition randomly or
384
384
  # based on the key when present.
385
- partition ||= -1
385
+ partition ||= Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
386
386
 
387
387
  # If timestamp is nil use 0 and let Kafka set one. If an integer or time
388
388
  # use it.
@@ -400,9 +400,9 @@ module Rdkafka
400
400
  delivery_handle.label = label
401
401
  delivery_handle.topic = topic
402
402
  delivery_handle[:pending] = true
403
- delivery_handle[:response] = -1
404
- delivery_handle[:partition] = -1
405
- delivery_handle[:offset] = -1
403
+ delivery_handle[:response] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
404
+ delivery_handle[:partition] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
405
+ delivery_handle[:offset] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
406
406
  DeliveryHandle.register(delivery_handle)
407
407
 
408
408
  args = [
@@ -454,9 +454,12 @@ module Rdkafka
454
454
  end
455
455
 
456
456
  # Raise error if the produce call was not successful
457
- if response != 0
457
+ if response != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
458
458
  DeliveryHandle.remove(delivery_handle.to_ptr.address)
459
- Rdkafka::RdkafkaError.validate!(response)
459
+
460
+ @native_kafka.with_inner do |inner|
461
+ Rdkafka::RdkafkaError.validate!(response, client_ptr: inner)
462
+ end
460
463
  end
461
464
 
462
465
  delivery_handle
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rdkafka
4
- VERSION = "0.23.0.beta2"
4
+ VERSION = "0.23.1.rc1"
5
5
  LIBRDKAFKA_VERSION = "2.12.1"
6
6
  LIBRDKAFKA_SOURCE_SHA256 = "ec103fa05cb0f251e375f6ea0b6112cfc9d0acd977dc5b69fdc54242ba38a16f"
7
7
  end
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.23.0.beta2
4
+ version: 0.23.1.rc1
5
5
  platform: arm64-darwin
6
6
  authors:
7
7
  - Thijs Cadier
@@ -222,6 +222,7 @@ files:
222
222
  - lib/rdkafka/producer/delivery_handle.rb
223
223
  - lib/rdkafka/producer/delivery_report.rb
224
224
  - lib/rdkafka/producer/partitions_count_cache.rb
225
+ - lib/rdkafka/producer/testing.rb
225
226
  - lib/rdkafka/version.rb
226
227
  - renovate.json
227
228
  licenses: