rdkafka 0.29.1 → 0.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/rdkafka/admin.rb +143 -85
- data/lib/rdkafka/bindings.rb +1 -0
- data/lib/rdkafka/clients.rb +114 -0
- data/lib/rdkafka/config.rb +84 -39
- data/lib/rdkafka/consumer/allocator.rb +38 -0
- data/lib/rdkafka/consumer/topic_partition_list.rb +1 -2
- data/lib/rdkafka/consumer.rb +44 -40
- data/lib/rdkafka/version.rb +1 -1
- data/lib/rdkafka.rb +4 -0
- data/package-lock.json +6 -6
- data/renovate.json +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2acf73116281ea22d96af51b56fecc615cb32616d25834a5a990fae1213a1ba
|
|
4
|
+
data.tar.gz: 04c06f93561b22719261fab54fc0191f1a6edba01926596124f430adcc11d7d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ea74276532d8cd48ee913a46166497ace6426bd8859eb41d72f5407bc5a1eb4273bbe2d82a793618e3420a956b95a86919e416c46e0ac657d8b8056167dcdc0
|
|
7
|
+
data.tar.gz: 20e3c8ac1bb1113673f5af90e977dc3e59e6e6b9f9b0b550e1f6b1a9796bd19b1915d8ba8e345e5cfb0e574d61e8fe354788e0f3445ee0333a8634c7f81e4d97
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Rdkafka Changelog
|
|
2
2
|
|
|
3
|
+
## 0.29.2 (2026-09-11)
|
|
4
|
+
- [Fix] Close live clients from an `at_exit` hook before Ruby's shutdown finalization, so librdkafka is no longer `dlclose`d while its native threads are still running (which could segfault on exit).
|
|
5
|
+
- [Fix] Make client construction exception-safe and destroy the native handle when setup fails after `rd_kafka_new`, so a later error no longer orphans the native client.
|
|
6
|
+
- [Fix] Destroy each polled message exactly once in `Consumer#poll_batch`/`#poll_batch_nb` when building a message raises a non-`RdkafkaError`, fixing a double-free that could abort the process and closing the matching leak window.
|
|
7
|
+
- [Fix] Free the background queue, `AdminOptions` and any already-built `ConfigResource`s (and remove the handle) when `Admin#describe_configs`/`#incremental_alter_configs` raise while building native resources (e.g. a non-String resource name).
|
|
8
|
+
- [Fix] Raise a clear `ConfigError` instead of segfaulting when `Admin#describe_configs`/`#incremental_alter_configs` are given an empty resource name or a negative resource type (`rd_kafka_ConfigResource_new` returns NULL), or when `rd_kafka_AdminOptions_new` returns NULL.
|
|
9
|
+
|
|
3
10
|
## 0.29.1 (2026-09-04)
|
|
4
11
|
- [Enhancement] Add `Admin#delete_records` to delete all messages in a partition up to a given offset.
|
|
5
12
|
- [Enhancement] Add `Admin#list_consumer_groups` to list the cluster's consumer groups.
|
|
@@ -8,6 +15,7 @@
|
|
|
8
15
|
- [Fix] Make `NativeKafka#close` fork-aware so a forked child no longer segfaults on exit.
|
|
9
16
|
- [Fix] Stabilize the `to_native_tpl` leak integration spec.
|
|
10
17
|
- [Fix] Stabilize the flaky partitions count cache statistics spec.
|
|
18
|
+
- [Fix] Use global allocator in topic_partition_list for pointer freed by librdkafka.
|
|
11
19
|
|
|
12
20
|
## 0.29.0 (2026-07-10)
|
|
13
21
|
- [Enhancement] Bump librdkafka to `2.14.2`
|
data/lib/rdkafka/admin.rb
CHANGED
|
@@ -898,35 +898,64 @@ module Rdkafka
|
|
|
898
898
|
handle[:pending] = true
|
|
899
899
|
handle[:response] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
900
900
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
901
|
+
# All native allocation happens inside the begin so a raise at any point (a null background
|
|
902
|
+
# queue, a non-String resource name making `from_string` raise mid-build, etc.) is cleaned up
|
|
903
|
+
# by the ensure rather than leaking the queue, the AdminOptions, the handle and the
|
|
904
|
+
# ConfigResources already built.
|
|
905
|
+
queue_ptr = nil
|
|
906
|
+
admin_options_ptr = nil
|
|
907
|
+
pointer_array = []
|
|
908
|
+
registered = false
|
|
904
909
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
910
|
+
begin
|
|
911
|
+
queue_ptr = @native_kafka.with_inner do |inner|
|
|
912
|
+
Rdkafka::Bindings.rd_kafka_queue_get_background(inner)
|
|
913
|
+
end
|
|
908
914
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_DESCRIBECONFIGS
|
|
913
|
-
)
|
|
914
|
-
end
|
|
915
|
+
if queue_ptr.null?
|
|
916
|
+
raise Rdkafka::Config::ConfigError.new("rd_kafka_queue_get_background was NULL")
|
|
917
|
+
end
|
|
915
918
|
|
|
916
|
-
|
|
917
|
-
|
|
919
|
+
admin_options_ptr = @native_kafka.with_inner do |inner|
|
|
920
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_new(
|
|
921
|
+
inner,
|
|
922
|
+
Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_DESCRIBECONFIGS
|
|
923
|
+
)
|
|
924
|
+
end
|
|
918
925
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
end
|
|
926
|
+
# A NULL AdminOptions would segfault the moment we set its opaque pointer, so reject it
|
|
927
|
+
# before registering the handle (the ensure frees the queue already acquired).
|
|
928
|
+
if admin_options_ptr.null?
|
|
929
|
+
raise Rdkafka::Config::ConfigError.new("rd_kafka_AdminOptions_new was NULL")
|
|
930
|
+
end
|
|
925
931
|
|
|
926
|
-
|
|
927
|
-
|
|
932
|
+
DescribeConfigsHandle.register(handle)
|
|
933
|
+
registered = true
|
|
934
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, handle.to_ptr)
|
|
935
|
+
|
|
936
|
+
# Build resources one at a time so a raise on a later element still leaves the earlier ones
|
|
937
|
+
# in pointer_array for the ensure to destroy.
|
|
938
|
+
parsed_resources.each do |resource_type, resource_name|
|
|
939
|
+
resource_ptr = Rdkafka::Bindings.rd_kafka_ConfigResource_new(
|
|
940
|
+
resource_type,
|
|
941
|
+
FFI::MemoryPointer.from_string(resource_name)
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
# librdkafka returns NULL for an empty resource name or a negative resource type. Passing
|
|
945
|
+
# NULL on to DescribeConfigs and then destroying it both dereference NULL and segfault, so
|
|
946
|
+
# reject it here (the ensure frees everything already built).
|
|
947
|
+
if resource_ptr.null?
|
|
948
|
+
raise Rdkafka::Config::ConfigError.new(
|
|
949
|
+
"rd_kafka_ConfigResource_new was NULL for #{resource_name.inspect} (type #{resource_type})"
|
|
950
|
+
)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
pointer_array << resource_ptr
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
configs_array_ptr = FFI::MemoryPointer.new(:pointer, pointer_array.size)
|
|
957
|
+
configs_array_ptr.write_array_of_pointer(pointer_array)
|
|
928
958
|
|
|
929
|
-
begin
|
|
930
959
|
@native_kafka.with_inner do |inner|
|
|
931
960
|
Rdkafka::Bindings.rd_kafka_DescribeConfigs(
|
|
932
961
|
inner,
|
|
@@ -937,18 +966,20 @@ module Rdkafka
|
|
|
937
966
|
)
|
|
938
967
|
end
|
|
939
968
|
rescue Exception
|
|
940
|
-
DescribeConfigsHandle.remove(handle.to_ptr.address)
|
|
969
|
+
DescribeConfigsHandle.remove(handle.to_ptr.address) if registered
|
|
941
970
|
|
|
942
971
|
raise
|
|
943
972
|
ensure
|
|
944
|
-
|
|
945
|
-
|
|
973
|
+
if admin_options_ptr && !admin_options_ptr.null?
|
|
974
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_destroy(admin_options_ptr)
|
|
975
|
+
end
|
|
946
976
|
|
|
947
|
-
if
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
977
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue_ptr) if queue_ptr && !queue_ptr.null?
|
|
978
|
+
|
|
979
|
+
pointer_array.each do |config_resource_ptr|
|
|
980
|
+
next if config_resource_ptr.null?
|
|
981
|
+
|
|
982
|
+
Rdkafka::Bindings.rd_kafka_ConfigResource_destroy(config_resource_ptr)
|
|
952
983
|
end
|
|
953
984
|
end
|
|
954
985
|
|
|
@@ -987,66 +1018,91 @@ module Rdkafka
|
|
|
987
1018
|
handle[:pending] = true
|
|
988
1019
|
handle[:response] = Rdkafka::Bindings::RD_KAFKA_PARTITION_UA
|
|
989
1020
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1021
|
+
# All native allocation happens inside the begin so a raise at any point (a null background
|
|
1022
|
+
# queue, a non-String resource name, or FFI marshaling of a config name/value failing while
|
|
1023
|
+
# adding configs) is cleaned up by the ensure rather than leaking the queue, the AdminOptions,
|
|
1024
|
+
# the handle and the ConfigResources already built.
|
|
1025
|
+
queue_ptr = nil
|
|
1026
|
+
admin_options_ptr = nil
|
|
1027
|
+
pointer_array = []
|
|
1028
|
+
registered = false
|
|
1029
|
+
add_error = nil
|
|
993
1030
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1031
|
+
begin
|
|
1032
|
+
queue_ptr = @native_kafka.with_inner do |inner|
|
|
1033
|
+
Rdkafka::Bindings.rd_kafka_queue_get_background(inner)
|
|
1034
|
+
end
|
|
997
1035
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_INCREMENTALALTERCONFIGS
|
|
1002
|
-
)
|
|
1003
|
-
end
|
|
1036
|
+
if queue_ptr.null?
|
|
1037
|
+
raise Rdkafka::Config::ConfigError.new("rd_kafka_queue_get_background was NULL")
|
|
1038
|
+
end
|
|
1004
1039
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1040
|
+
admin_options_ptr = @native_kafka.with_inner do |inner|
|
|
1041
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_new(
|
|
1042
|
+
inner,
|
|
1043
|
+
Rdkafka::Bindings::RD_KAFKA_ADMIN_OP_INCREMENTALALTERCONFIGS
|
|
1044
|
+
)
|
|
1045
|
+
end
|
|
1007
1046
|
|
|
1008
|
-
|
|
1047
|
+
# A NULL AdminOptions would segfault the moment we set its opaque pointer, so reject it
|
|
1048
|
+
# before registering the handle (the ensure frees the queue already acquired).
|
|
1049
|
+
if admin_options_ptr.null?
|
|
1050
|
+
raise Rdkafka::Config::ConfigError.new("rd_kafka_AdminOptions_new was NULL")
|
|
1051
|
+
end
|
|
1009
1052
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
resource_type,
|
|
1014
|
-
FFI::MemoryPointer.from_string(resource_name)
|
|
1015
|
-
)
|
|
1053
|
+
IncrementalAlterConfigsHandle.register(handle)
|
|
1054
|
+
registered = true
|
|
1055
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_set_opaque(admin_options_ptr, handle.to_ptr)
|
|
1016
1056
|
|
|
1017
|
-
|
|
1018
|
-
#
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
# native error object) and raise it below, before the request is sent.
|
|
1023
|
-
error_ptr = Bindings.rd_kafka_ConfigResource_add_incremental_config(
|
|
1024
|
-
resource_ptr,
|
|
1025
|
-
name,
|
|
1026
|
-
op_type,
|
|
1027
|
-
value
|
|
1057
|
+
parsed_resources.each do |resource_type, resource_name, configs|
|
|
1058
|
+
# First build the appropriate resource representation
|
|
1059
|
+
resource_ptr = Rdkafka::Bindings.rd_kafka_ConfigResource_new(
|
|
1060
|
+
resource_type,
|
|
1061
|
+
FFI::MemoryPointer.from_string(resource_name)
|
|
1028
1062
|
)
|
|
1029
1063
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1064
|
+
# librdkafka returns NULL for an empty resource name or a negative resource type. Passing
|
|
1065
|
+
# NULL on to add_incremental_config/IncrementalAlterConfigs and then destroying it both
|
|
1066
|
+
# dereference NULL and segfault, so reject it here (the ensure frees everything built).
|
|
1067
|
+
if resource_ptr.null?
|
|
1068
|
+
raise Rdkafka::Config::ConfigError.new(
|
|
1069
|
+
"rd_kafka_ConfigResource_new was NULL for #{resource_name.inspect} (type #{resource_type})"
|
|
1070
|
+
)
|
|
1071
|
+
end
|
|
1033
1072
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1073
|
+
# Track it immediately so a raise while adding its configs still frees it via the ensure.
|
|
1074
|
+
pointer_array << resource_ptr
|
|
1075
|
+
|
|
1076
|
+
configs.each do |name, op_type, value|
|
|
1077
|
+
# rd_kafka_ConfigResource_add_incremental_config returns a non-NULL rd_kafka_error_t for
|
|
1078
|
+
# an invalid op_type, an empty/nil name, or a nil value on a non-delete op. The result
|
|
1079
|
+
# used to be ignored: the entry was silently dropped, the alter request still reported
|
|
1080
|
+
# success, and the error object leaked. Capture the first error (always destroying the
|
|
1081
|
+
# native error object) and raise it below, before the request is sent.
|
|
1082
|
+
error_ptr = Bindings.rd_kafka_ConfigResource_add_incremental_config(
|
|
1083
|
+
resource_ptr,
|
|
1084
|
+
name,
|
|
1085
|
+
op_type,
|
|
1086
|
+
value
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
unless error_ptr.null?
|
|
1090
|
+
code = Rdkafka::Bindings.rd_kafka_error_code(error_ptr)
|
|
1091
|
+
Rdkafka::Bindings.rd_kafka_error_destroy(error_ptr)
|
|
1092
|
+
|
|
1093
|
+
unless code == Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
1094
|
+
add_error ||= Rdkafka::RdkafkaError.new(
|
|
1095
|
+
code,
|
|
1096
|
+
"rd_kafka_ConfigResource_add_incremental_config"
|
|
1097
|
+
)
|
|
1098
|
+
end
|
|
1039
1099
|
end
|
|
1040
1100
|
end
|
|
1041
1101
|
end
|
|
1042
1102
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
configs_array_ptr = FFI::MemoryPointer.new(:pointer, pointer_array.size)
|
|
1047
|
-
configs_array_ptr.write_array_of_pointer(pointer_array)
|
|
1103
|
+
configs_array_ptr = FFI::MemoryPointer.new(:pointer, pointer_array.size)
|
|
1104
|
+
configs_array_ptr.write_array_of_pointer(pointer_array)
|
|
1048
1105
|
|
|
1049
|
-
begin
|
|
1050
1106
|
# Raise only after the full array is built so the ensure below frees every ConfigResource
|
|
1051
1107
|
# we created, and before the request is sent so a rejected entry can't report success.
|
|
1052
1108
|
raise add_error if add_error
|
|
@@ -1061,18 +1117,20 @@ module Rdkafka
|
|
|
1061
1117
|
)
|
|
1062
1118
|
end
|
|
1063
1119
|
rescue Exception
|
|
1064
|
-
IncrementalAlterConfigsHandle.remove(handle.to_ptr.address)
|
|
1120
|
+
IncrementalAlterConfigsHandle.remove(handle.to_ptr.address) if registered
|
|
1065
1121
|
|
|
1066
1122
|
raise
|
|
1067
1123
|
ensure
|
|
1068
|
-
|
|
1069
|
-
|
|
1124
|
+
if admin_options_ptr && !admin_options_ptr.null?
|
|
1125
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_destroy(admin_options_ptr)
|
|
1126
|
+
end
|
|
1070
1127
|
|
|
1071
|
-
if
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1128
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue_ptr) if queue_ptr && !queue_ptr.null?
|
|
1129
|
+
|
|
1130
|
+
pointer_array.each do |config_resource_ptr|
|
|
1131
|
+
next if config_resource_ptr.null?
|
|
1132
|
+
|
|
1133
|
+
Rdkafka::Bindings.rd_kafka_ConfigResource_destroy(config_resource_ptr)
|
|
1076
1134
|
end
|
|
1077
1135
|
end
|
|
1078
1136
|
|
data/lib/rdkafka/bindings.rb
CHANGED
|
@@ -161,6 +161,7 @@ module Rdkafka
|
|
|
161
161
|
|
|
162
162
|
attach_function :rd_kafka_DescribeConfigs, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
|
|
163
163
|
attach_function :rd_kafka_ConfigResource_new, [:int32, :pointer], :pointer
|
|
164
|
+
attach_function :rd_kafka_ConfigResource_destroy, [:pointer], :void
|
|
164
165
|
attach_function :rd_kafka_ConfigResource_destroy_array, [:pointer, :int32], :void
|
|
165
166
|
attach_function :rd_kafka_event_DescribeConfigs_result, [:pointer], :pointer
|
|
166
167
|
attach_function :rd_kafka_DescribeConfigs_result_resources, [:pointer, :pointer], :pointer
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdkafka
|
|
4
|
+
# Tracks live public clients so they can be closed before Ruby begins forced finalization.
|
|
5
|
+
# @private
|
|
6
|
+
module Clients
|
|
7
|
+
# Ruby rewrote `ObjectSpace::WeakMap` in 3.3. On 3.2 and earlier its `[]` can hand back a
|
|
8
|
+
# foreign or already-freed object for a key, which raises `NoMethodError` or segfaults the VM
|
|
9
|
+
# the moment the value is touched. Track weakly only where that is safe and fall back to a
|
|
10
|
+
# strong registry - pruned as clients close - on older Rubies. The strong registry holds an
|
|
11
|
+
# open, un-closed client until shutdown, which is exactly when this cleanup needs it anyway.
|
|
12
|
+
WEAK_TRACKING = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.3")
|
|
13
|
+
|
|
14
|
+
@pid = Process.pid
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
|
|
17
|
+
if WEAK_TRACKING
|
|
18
|
+
@clients = ObjectSpace::WeakMap.new
|
|
19
|
+
@tokens = [].freeze
|
|
20
|
+
else
|
|
21
|
+
@clients = {}.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Registers a successfully constructed client so it can be closed during process shutdown.
|
|
25
|
+
# @param client [Consumer, Producer, Admin] client to close during process shutdown
|
|
26
|
+
# @return [Consumer, Producer, Admin] the registered client
|
|
27
|
+
def self.register(client)
|
|
28
|
+
reset_after_fork
|
|
29
|
+
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
if WEAK_TRACKING
|
|
32
|
+
live_clients
|
|
33
|
+
|
|
34
|
+
# The strongly-held token is the key and the client is the weak value, so registering a
|
|
35
|
+
# client never keeps it alive. Enumerating weak keys can expose reclaimed slots, so we
|
|
36
|
+
# look the value up by token instead.
|
|
37
|
+
token = Object.new
|
|
38
|
+
@clients[token] = client
|
|
39
|
+
@tokens = (@tokens + [token]).freeze
|
|
40
|
+
else
|
|
41
|
+
# Strong references, so drop clients that have already been closed to bound growth.
|
|
42
|
+
live = @clients.reject { |_id, existing| existing.closed? }
|
|
43
|
+
live[client.object_id] = client
|
|
44
|
+
@clients = live.freeze
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
client
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Closes each live client registered in this process.
|
|
52
|
+
# @return [nil]
|
|
53
|
+
# @raise [StandardError] the first close error, after attempting every client
|
|
54
|
+
def self.close_all
|
|
55
|
+
return unless @pid == Process.pid
|
|
56
|
+
|
|
57
|
+
clients = @mutex.synchronize { current_clients }
|
|
58
|
+
first_error = nil
|
|
59
|
+
|
|
60
|
+
clients.each do |client|
|
|
61
|
+
client.close unless client.closed?
|
|
62
|
+
rescue => error
|
|
63
|
+
first_error ||= error
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
raise first_error if first_error
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns the currently registered live clients.
|
|
70
|
+
# @return [Array<Consumer, Producer, Admin>] live clients
|
|
71
|
+
# @private
|
|
72
|
+
def self.current_clients
|
|
73
|
+
WEAK_TRACKING ? live_clients : @clients.values
|
|
74
|
+
end
|
|
75
|
+
private_class_method :current_clients
|
|
76
|
+
|
|
77
|
+
# Returns the live clients and releases tokens whose clients have been collected.
|
|
78
|
+
# Weak-tracking Rubies only.
|
|
79
|
+
# @return [Array<Consumer, Producer, Admin>] live clients
|
|
80
|
+
# @private
|
|
81
|
+
def self.live_clients
|
|
82
|
+
clients = []
|
|
83
|
+
tokens = []
|
|
84
|
+
|
|
85
|
+
@tokens.each do |token|
|
|
86
|
+
client = @clients[token]
|
|
87
|
+
clients << client if client
|
|
88
|
+
tokens << token if client
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@tokens = tokens.freeze
|
|
92
|
+
clients
|
|
93
|
+
end
|
|
94
|
+
private_class_method :live_clients
|
|
95
|
+
|
|
96
|
+
# Replaces inherited state before registering a client in a forked child.
|
|
97
|
+
# @return [nil]
|
|
98
|
+
# @private
|
|
99
|
+
def self.reset_after_fork
|
|
100
|
+
return if @pid == Process.pid
|
|
101
|
+
|
|
102
|
+
if WEAK_TRACKING
|
|
103
|
+
@clients = ObjectSpace::WeakMap.new
|
|
104
|
+
@tokens = [].freeze
|
|
105
|
+
else
|
|
106
|
+
@clients = {}.freeze
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@pid = Process.pid
|
|
110
|
+
@mutex = Mutex.new
|
|
111
|
+
end
|
|
112
|
+
private_class_method :reset_after_fork
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/rdkafka/config.rb
CHANGED
|
@@ -209,21 +209,16 @@ module Rdkafka
|
|
|
209
209
|
Rdkafka::Bindings.rd_kafka_conf_set_rebalance_cb(config, Rdkafka::Bindings::RebalanceCallback)
|
|
210
210
|
end
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Rdkafka::
|
|
221
|
-
|
|
222
|
-
run_polling_thread: false,
|
|
223
|
-
opaque: opaque,
|
|
224
|
-
auto_start: native_kafka_auto_start
|
|
225
|
-
)
|
|
226
|
-
)
|
|
212
|
+
build_native_client(
|
|
213
|
+
config,
|
|
214
|
+
:rd_kafka_consumer,
|
|
215
|
+
opaque: opaque,
|
|
216
|
+
run_polling_thread: false,
|
|
217
|
+
auto_start: native_kafka_auto_start
|
|
218
|
+
) do |kafka, native_kafka|
|
|
219
|
+
Rdkafka::Bindings.rd_kafka_poll_set_consumer(kafka) if @consumer_poll_set
|
|
220
|
+
Rdkafka::Consumer.new(native_kafka)
|
|
221
|
+
end
|
|
227
222
|
end
|
|
228
223
|
|
|
229
224
|
# Create a producer with this configuration.
|
|
@@ -247,19 +242,17 @@ module Rdkafka
|
|
|
247
242
|
# Return producer with Kafka client
|
|
248
243
|
partitioner_name = self[:partitioner] || self["partitioner"]
|
|
249
244
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
).tap do |producer|
|
|
262
|
-
opaque.producer = producer
|
|
245
|
+
build_native_client(
|
|
246
|
+
config,
|
|
247
|
+
:rd_kafka_producer,
|
|
248
|
+
opaque: opaque,
|
|
249
|
+
run_polling_thread: run_polling_thread,
|
|
250
|
+
auto_start: native_kafka_auto_start,
|
|
251
|
+
timeout_ms: native_kafka_poll_timeout_ms
|
|
252
|
+
) do |_kafka, native_kafka|
|
|
253
|
+
Rdkafka::Producer.new(native_kafka, partitioner_name).tap do |producer|
|
|
254
|
+
opaque.producer = producer
|
|
255
|
+
end
|
|
263
256
|
end
|
|
264
257
|
end
|
|
265
258
|
|
|
@@ -279,17 +272,16 @@ module Rdkafka
|
|
|
279
272
|
config = native_config(opaque)
|
|
280
273
|
Rdkafka::Bindings.rd_kafka_conf_set_background_event_cb(config, Rdkafka::Callbacks::BackgroundEventCallbackFunction)
|
|
281
274
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
)
|
|
275
|
+
build_native_client(
|
|
276
|
+
config,
|
|
277
|
+
:rd_kafka_producer,
|
|
278
|
+
opaque: opaque,
|
|
279
|
+
run_polling_thread: run_polling_thread,
|
|
280
|
+
auto_start: native_kafka_auto_start,
|
|
281
|
+
timeout_ms: native_kafka_poll_timeout_ms
|
|
282
|
+
) do |_kafka, native_kafka|
|
|
283
|
+
Rdkafka::Admin.new(native_kafka)
|
|
284
|
+
end
|
|
293
285
|
end
|
|
294
286
|
|
|
295
287
|
# Returns all configuration properties and their current values for this config.
|
|
@@ -343,6 +335,56 @@ module Rdkafka
|
|
|
343
335
|
|
|
344
336
|
private
|
|
345
337
|
|
|
338
|
+
# Builds a public client around a native Kafka handle, destroying the current owner if setup
|
|
339
|
+
# fails before ownership is transferred to the caller.
|
|
340
|
+
# @param config [FFI::Pointer] native rdkafka configuration
|
|
341
|
+
# @param type [Symbol] native client type
|
|
342
|
+
# @param opaque [Object] callback context retained for the native client
|
|
343
|
+
# @param run_polling_thread [Boolean] whether to run a Ruby polling thread
|
|
344
|
+
# @param auto_start [Boolean] whether to start the polling thread after wrapping the client
|
|
345
|
+
# @param timeout_ms [Integer] polling timeout in milliseconds
|
|
346
|
+
# @yieldparam kafka [FFI::Pointer] native Kafka handle
|
|
347
|
+
# @yieldparam native_kafka [NativeKafka] Ruby owner of the native handle
|
|
348
|
+
# @return [Consumer, Producer, Admin] the constructed public client
|
|
349
|
+
# @private
|
|
350
|
+
def build_native_client(
|
|
351
|
+
config,
|
|
352
|
+
type,
|
|
353
|
+
opaque:,
|
|
354
|
+
run_polling_thread:,
|
|
355
|
+
auto_start:,
|
|
356
|
+
timeout_ms: Defaults::NATIVE_KAFKA_POLL_TIMEOUT_MS
|
|
357
|
+
)
|
|
358
|
+
kafka = native_kafka(config, type)
|
|
359
|
+
native_kafka = nil
|
|
360
|
+
client = nil
|
|
361
|
+
|
|
362
|
+
begin
|
|
363
|
+
native_kafka = Rdkafka::NativeKafka.new(
|
|
364
|
+
kafka,
|
|
365
|
+
run_polling_thread: run_polling_thread,
|
|
366
|
+
opaque: opaque,
|
|
367
|
+
auto_start: false,
|
|
368
|
+
timeout_ms: timeout_ms
|
|
369
|
+
)
|
|
370
|
+
client = yield(kafka, native_kafka)
|
|
371
|
+
native_kafka.start if auto_start
|
|
372
|
+
Rdkafka::Clients.register(client)
|
|
373
|
+
rescue Exception
|
|
374
|
+
owner = client || native_kafka
|
|
375
|
+
|
|
376
|
+
begin
|
|
377
|
+
owner ? owner.close : Rdkafka::Bindings.rd_kafka_destroy(kafka)
|
|
378
|
+
rescue Exception => err
|
|
379
|
+
# Never let a teardown failure replace the error that caused it: that error is what the
|
|
380
|
+
# caller needs to see, and it is re-raised below regardless.
|
|
381
|
+
Rdkafka::Config.logger.error("Unhandled exception: #{err.class} - #{err.message}")
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
raise
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
346
388
|
# This method is only intended to be used to create a client,
|
|
347
389
|
# using it in another way will leak memory.
|
|
348
390
|
#
|
|
@@ -429,6 +471,9 @@ module Rdkafka
|
|
|
429
471
|
|
|
430
472
|
# Return handle which should be closed using rd_kafka_destroy after usage.
|
|
431
473
|
handle
|
|
474
|
+
rescue Exception
|
|
475
|
+
Rdkafka::Bindings.rd_kafka_destroy(handle) if handle && !handle.null?
|
|
476
|
+
raise
|
|
432
477
|
end
|
|
433
478
|
end
|
|
434
479
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rdkafka
|
|
4
|
+
class Consumer
|
|
5
|
+
# Creates FFI pointers from the process-global allocator so that memory handed to librdkafka
|
|
6
|
+
# can be released by it with the matching `free`.
|
|
7
|
+
module Allocator
|
|
8
|
+
extend FFI::Library
|
|
9
|
+
|
|
10
|
+
ffi_lib FFI::CURRENT_PROCESS # use process-global malloc not FFI::Library::LIBC
|
|
11
|
+
attach_function :malloc, [:size_t], :pointer
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Return an FFI pointer to a string allocated from the global allocator
|
|
15
|
+
# so that it can be released by another library.
|
|
16
|
+
#
|
|
17
|
+
# @private
|
|
18
|
+
#
|
|
19
|
+
# @param string [String] Ruby string that will be copied into the new
|
|
20
|
+
# string pointer.
|
|
21
|
+
#
|
|
22
|
+
# @return [FFI::Pointer]
|
|
23
|
+
def string_pointer(string)
|
|
24
|
+
bytes = string.to_s
|
|
25
|
+
pointer = malloc(bytes.bytesize + 1)
|
|
26
|
+
Kernel.raise(NoMemoryError, "malloc failed") if pointer.null?
|
|
27
|
+
|
|
28
|
+
pointer.put_bytes(0, bytes)
|
|
29
|
+
pointer.put_char(bytes.bytesize, 0) # NUL terminator
|
|
30
|
+
|
|
31
|
+
pointer.autorelease = false
|
|
32
|
+
|
|
33
|
+
pointer
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -160,10 +160,9 @@ module Rdkafka
|
|
|
160
160
|
|
|
161
161
|
if p.metadata
|
|
162
162
|
part = Rdkafka::Bindings::TopicPartition.new(ref)
|
|
163
|
-
str_ptr = FFI::MemoryPointer.from_string(p.metadata)
|
|
164
163
|
# The metadata string is owned by librdkafka once handed over and released here:
|
|
165
164
|
# https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
|
|
166
|
-
str_ptr
|
|
165
|
+
str_ptr = Allocator.string_pointer(p.metadata)
|
|
167
166
|
part[:metadata] = str_ptr
|
|
168
167
|
part[:metadata_size] = p.metadata.bytesize
|
|
169
168
|
end
|
data/lib/rdkafka/consumer.rb
CHANGED
|
@@ -916,33 +916,35 @@ module Rdkafka
|
|
|
916
916
|
begin
|
|
917
917
|
while i < count
|
|
918
918
|
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
919
|
+
# Take ownership of this index before processing the pointer: advancing `i` up front means
|
|
920
|
+
# the cleanup loop in the `ensure` below starts past it, so a raise mid-iteration cannot
|
|
921
|
+
# double-free the same message.
|
|
922
|
+
i += 1
|
|
919
923
|
|
|
920
|
-
if ptr.null?
|
|
921
|
-
i += 1
|
|
922
|
-
next
|
|
923
|
-
end
|
|
924
|
+
next if ptr.null?
|
|
924
925
|
|
|
925
|
-
|
|
926
|
+
# One `ensure` around the whole body guarantees `ptr` is destroyed exactly once on every
|
|
927
|
+
# path - the error branch, a successful build, a rescued `RdkafkaError`, or any other
|
|
928
|
+
# exception unwinding through here - so nothing leaks in the window before the build.
|
|
929
|
+
begin
|
|
930
|
+
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
926
931
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
next
|
|
932
|
-
end
|
|
932
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
933
|
+
results << Rdkafka::RdkafkaError.new(native_message[:err])
|
|
934
|
+
next
|
|
935
|
+
end
|
|
933
936
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
937
|
+
begin
|
|
938
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
939
|
+
rescue Rdkafka::RdkafkaError => e
|
|
940
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
941
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
942
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
943
|
+
results << e
|
|
944
|
+
end
|
|
941
945
|
ensure
|
|
942
946
|
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
943
947
|
end
|
|
944
|
-
|
|
945
|
-
i += 1
|
|
946
948
|
end
|
|
947
949
|
ensure
|
|
948
950
|
while i < count
|
|
@@ -991,33 +993,35 @@ module Rdkafka
|
|
|
991
993
|
begin
|
|
992
994
|
while i < count
|
|
993
995
|
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
996
|
+
# Take ownership of this index before processing the pointer: advancing `i` up front means
|
|
997
|
+
# the cleanup loop in the `ensure` below starts past it, so a raise mid-iteration cannot
|
|
998
|
+
# double-free the same message.
|
|
999
|
+
i += 1
|
|
994
1000
|
|
|
995
|
-
if ptr.null?
|
|
996
|
-
i += 1
|
|
997
|
-
next
|
|
998
|
-
end
|
|
1001
|
+
next if ptr.null?
|
|
999
1002
|
|
|
1000
|
-
|
|
1003
|
+
# One `ensure` around the whole body guarantees `ptr` is destroyed exactly once on every
|
|
1004
|
+
# path - the error branch, a successful build, a rescued `RdkafkaError`, or any other
|
|
1005
|
+
# exception unwinding through here - so nothing leaks in the window before the build.
|
|
1006
|
+
begin
|
|
1007
|
+
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
1001
1008
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
next
|
|
1007
|
-
end
|
|
1009
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
1010
|
+
results << Rdkafka::RdkafkaError.new(native_message[:err])
|
|
1011
|
+
next
|
|
1012
|
+
end
|
|
1008
1013
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1014
|
+
begin
|
|
1015
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
1016
|
+
rescue Rdkafka::RdkafkaError => e
|
|
1017
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
1018
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
1019
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
1020
|
+
results << e
|
|
1021
|
+
end
|
|
1016
1022
|
ensure
|
|
1017
1023
|
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
1018
1024
|
end
|
|
1019
|
-
|
|
1020
|
-
i += 1
|
|
1021
1025
|
end
|
|
1022
1026
|
ensure
|
|
1023
1027
|
while i < count
|
data/lib/rdkafka/version.rb
CHANGED
data/lib/rdkafka.rb
CHANGED
|
@@ -60,6 +60,7 @@ require "rdkafka/consumer"
|
|
|
60
60
|
require "rdkafka/consumer/headers"
|
|
61
61
|
require "rdkafka/consumer/message"
|
|
62
62
|
require "rdkafka/consumer/partition"
|
|
63
|
+
require "rdkafka/consumer/allocator"
|
|
63
64
|
require "rdkafka/consumer/topic_partition_list"
|
|
64
65
|
require "rdkafka/error"
|
|
65
66
|
require "rdkafka/metadata"
|
|
@@ -68,9 +69,12 @@ require "rdkafka/producer/partitions_count_cache"
|
|
|
68
69
|
require "rdkafka/producer"
|
|
69
70
|
require "rdkafka/producer/delivery_handle"
|
|
70
71
|
require "rdkafka/producer/delivery_report"
|
|
72
|
+
require "rdkafka/clients"
|
|
71
73
|
|
|
72
74
|
# Main Rdkafka namespace of this gem
|
|
73
75
|
module Rdkafka
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
Rdkafka::Bindings.rd_kafka_global_init
|
|
79
|
+
|
|
80
|
+
at_exit { Rdkafka::Clients.close_all }
|
data/package-lock.json
CHANGED
|
@@ -103,9 +103,9 @@
|
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
105
|
"node_modules/fastq": {
|
|
106
|
-
"version": "1.20.
|
|
107
|
-
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.
|
|
108
|
-
"integrity": "sha512-
|
|
106
|
+
"version": "1.20.2",
|
|
107
|
+
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.2.tgz",
|
|
108
|
+
"integrity": "sha512-UpGiiODyCGprM8EPP6JodP6jC9Rws6TCuiDOD+nn0CJhR8guI3g/ozo4ugL0vJ+Yz1UtJuuRPqvQuybVOF1VQA==",
|
|
109
109
|
"dev": true,
|
|
110
110
|
"license": "ISC",
|
|
111
111
|
"dependencies": {
|
|
@@ -286,9 +286,9 @@
|
|
|
286
286
|
}
|
|
287
287
|
},
|
|
288
288
|
"node_modules/smol-toml": {
|
|
289
|
-
"version": "1.
|
|
290
|
-
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.
|
|
291
|
-
"integrity": "sha512-
|
|
289
|
+
"version": "1.8.0",
|
|
290
|
+
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz",
|
|
291
|
+
"integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==",
|
|
292
292
|
"dev": true,
|
|
293
293
|
"license": "BSD-3-Clause",
|
|
294
294
|
"engines": {
|
data/renovate.json
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rdkafka
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.29.
|
|
4
|
+
version: 0.29.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thijs Cadier
|
|
@@ -143,8 +143,10 @@ files:
|
|
|
143
143
|
- lib/rdkafka/callbacks/incremental_alter_configs_handler.rb
|
|
144
144
|
- lib/rdkafka/callbacks/list_consumer_groups_handler.rb
|
|
145
145
|
- lib/rdkafka/callbacks/list_offsets_handler.rb
|
|
146
|
+
- lib/rdkafka/clients.rb
|
|
146
147
|
- lib/rdkafka/config.rb
|
|
147
148
|
- lib/rdkafka/consumer.rb
|
|
149
|
+
- lib/rdkafka/consumer/allocator.rb
|
|
148
150
|
- lib/rdkafka/consumer/headers.rb
|
|
149
151
|
- lib/rdkafka/consumer/message.rb
|
|
150
152
|
- lib/rdkafka/consumer/partition.rb
|