karafka-rdkafka 0.28.1-aarch64-linux-gnu → 0.28.2-aarch64-linux-gnu
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 +9 -0
- data/ext/librdkafka.so +0 -0
- data/lib/rdkafka/admin.rb +140 -82
- 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 +2 -3
- 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: c55da5cc4ca09c9f37305039ff4e042da143270bbd2ca7be3e32821c2bab06c9
|
|
4
|
+
data.tar.gz: 06b1da56a0467dfecd246c9b32916875eac10f1adb050332a58da7063f02d169
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68dada1abf386a99173495683243015e6910c1e9fdb06009f6496251aeefd7007395c199d50f48c17968ce982d03b8acc41dff5779fa14a28b822a6dabb0daab
|
|
7
|
+
data.tar.gz: 406ba779b17fb15d8384979ac9f58d666e020848ae973366905e37794c6e3bec3b2935e589ded09cf4fee40d3d1eb135fe9e0e82e9620311d87335c050db5760
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Rdkafka Changelog
|
|
2
2
|
|
|
3
|
+
## 0.28.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). Ported from rdkafka-ruby (#964, Alex Selesse).
|
|
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. Ported from rdkafka-ruby (#964, Alex Selesse).
|
|
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. Ported from rdkafka-ruby (#973).
|
|
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). Ported from rdkafka-ruby (#973).
|
|
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. Ported from rdkafka-ruby (#973).
|
|
9
|
+
- [Fix] Allocate the topic-partition metadata string with a process-global malloc instead of an autorelease-off `FFI::MemoryPointer`, so the buffer handed to librdkafka is not freed under it. Ported from rdkafka-ruby (#969, Randy Stauner).
|
|
10
|
+
- [Maintenance] Bump the bundled OpenSSL used by the precompiled builds to `3.5.8` (LTS). Ported from rdkafka-ruby (#972, Scott Francis).
|
|
11
|
+
|
|
3
12
|
## v0.28.1 (2026-09-04)
|
|
4
13
|
- [Enhancement] Add `Admin#delete_records`, wrapping librdkafka's `DeleteRecords` admin API. Deletes all messages in a partition up to (but not including) a given offset - accepts an integer offset or `:end` to clear all current data. Ported from rdkafka-ruby (#956).
|
|
5
14
|
- [Enhancement] Add `Admin#list_consumer_groups`, wrapping librdkafka's `ListConsumerGroups` admin API. Returns a cluster-wide listing of consumer groups (`group_id`, `is_simple_consumer_group`, state) plus any per-broker errors. Ported from rdkafka-ruby (#955).
|
data/ext/librdkafka.so
CHANGED
|
Binary file
|
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,61 +1018,86 @@ 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
|
-
# destroys the 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
|
-
|
|
1033
|
-
|
|
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
|
|
1034
1072
|
|
|
1035
|
-
|
|
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 (build_from_c also
|
|
1081
|
+
# destroys the 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
|
+
error = Rdkafka::RdkafkaError.build_from_c(
|
|
1090
|
+
error_ptr,
|
|
1091
|
+
"rd_kafka_ConfigResource_add_incremental_config"
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
add_error ||= error if error
|
|
1095
|
+
end
|
|
1036
1096
|
end
|
|
1037
1097
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
configs_array_ptr = FFI::MemoryPointer.new(:pointer, pointer_array.size)
|
|
1042
|
-
configs_array_ptr.write_array_of_pointer(pointer_array)
|
|
1098
|
+
configs_array_ptr = FFI::MemoryPointer.new(:pointer, pointer_array.size)
|
|
1099
|
+
configs_array_ptr.write_array_of_pointer(pointer_array)
|
|
1043
1100
|
|
|
1044
|
-
begin
|
|
1045
1101
|
# Raise only after the full array is built so the ensure below frees every ConfigResource
|
|
1046
1102
|
# we created, and before the request is sent so a rejected entry can't report success.
|
|
1047
1103
|
raise add_error if add_error
|
|
@@ -1056,18 +1112,20 @@ module Rdkafka
|
|
|
1056
1112
|
)
|
|
1057
1113
|
end
|
|
1058
1114
|
rescue Exception
|
|
1059
|
-
IncrementalAlterConfigsHandle.remove(handle.to_ptr.address)
|
|
1115
|
+
IncrementalAlterConfigsHandle.remove(handle.to_ptr.address) if registered
|
|
1060
1116
|
|
|
1061
1117
|
raise
|
|
1062
1118
|
ensure
|
|
1063
|
-
|
|
1064
|
-
|
|
1119
|
+
if admin_options_ptr && !admin_options_ptr.null?
|
|
1120
|
+
Rdkafka::Bindings.rd_kafka_AdminOptions_destroy(admin_options_ptr)
|
|
1121
|
+
end
|
|
1065
1122
|
|
|
1066
|
-
if
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1123
|
+
Rdkafka::Bindings.rd_kafka_queue_destroy(queue_ptr) if queue_ptr && !queue_ptr.null?
|
|
1124
|
+
|
|
1125
|
+
pointer_array.each do |config_resource_ptr|
|
|
1126
|
+
next if config_resource_ptr.null?
|
|
1127
|
+
|
|
1128
|
+
Rdkafka::Bindings.rd_kafka_ConfigResource_destroy(config_resource_ptr)
|
|
1071
1129
|
end
|
|
1072
1130
|
end
|
|
1073
1131
|
|
data/lib/rdkafka/bindings.rb
CHANGED
|
@@ -170,6 +170,7 @@ module Rdkafka
|
|
|
170
170
|
|
|
171
171
|
attach_function :rd_kafka_DescribeConfigs, [:pointer, :pointer, :size_t, :pointer, :pointer], :void, blocking: true
|
|
172
172
|
attach_function :rd_kafka_ConfigResource_new, [:int32, :pointer], :pointer
|
|
173
|
+
attach_function :rd_kafka_ConfigResource_destroy, [:pointer], :void
|
|
173
174
|
attach_function :rd_kafka_ConfigResource_destroy_array, [:pointer, :int32], :void
|
|
174
175
|
attach_function :rd_kafka_event_DescribeConfigs_result, [:pointer], :pointer
|
|
175
176
|
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
|
|
@@ -162,10 +162,9 @@ module Rdkafka
|
|
|
162
162
|
# Remove the respond to check after karafka 2.3.0 is released
|
|
163
163
|
if p.respond_to?(:metadata) && p.metadata
|
|
164
164
|
part = Rdkafka::Bindings::TopicPartition.new(ref)
|
|
165
|
-
|
|
166
|
-
# released here:
|
|
165
|
+
# The metadata string is owned by librdkafka once handed over and released here:
|
|
167
166
|
# https://github.com/confluentinc/librdkafka/blob/e03d3bb91ed92a38f38d9806b8d8deffe78a1de5/src/rdkafka_partition.c#L2682C18-L2682C18
|
|
168
|
-
str_ptr
|
|
167
|
+
str_ptr = Allocator.string_pointer(p.metadata)
|
|
169
168
|
part[:metadata] = str_ptr
|
|
170
169
|
part[:metadata_size] = p.metadata.bytesize
|
|
171
170
|
end
|
data/lib/rdkafka/consumer.rb
CHANGED
|
@@ -906,33 +906,35 @@ module Rdkafka
|
|
|
906
906
|
begin
|
|
907
907
|
while i < count
|
|
908
908
|
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
909
|
+
# Take ownership of this index before processing the pointer: advancing `i` up front means
|
|
910
|
+
# the cleanup loop in the `ensure` below starts past it, so a raise mid-iteration cannot
|
|
911
|
+
# double-free the same message.
|
|
912
|
+
i += 1
|
|
909
913
|
|
|
910
|
-
if ptr.null?
|
|
911
|
-
i += 1
|
|
912
|
-
next
|
|
913
|
-
end
|
|
914
|
+
next if ptr.null?
|
|
914
915
|
|
|
915
|
-
|
|
916
|
+
# One `ensure` around the whole body guarantees `ptr` is destroyed exactly once on every
|
|
917
|
+
# path - the error branch, a successful build, a rescued `RdkafkaError`, or any other
|
|
918
|
+
# exception unwinding through here - so nothing leaks in the window before the build.
|
|
919
|
+
begin
|
|
920
|
+
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
916
921
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
next
|
|
922
|
-
end
|
|
922
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
923
|
+
results << build_batch_error(native_message)
|
|
924
|
+
next
|
|
925
|
+
end
|
|
923
926
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
927
|
+
begin
|
|
928
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
929
|
+
rescue Rdkafka::RdkafkaError => e
|
|
930
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
931
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
932
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
933
|
+
results << e
|
|
934
|
+
end
|
|
931
935
|
ensure
|
|
932
936
|
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
933
937
|
end
|
|
934
|
-
|
|
935
|
-
i += 1
|
|
936
938
|
end
|
|
937
939
|
ensure
|
|
938
940
|
while i < count
|
|
@@ -981,33 +983,35 @@ module Rdkafka
|
|
|
981
983
|
begin
|
|
982
984
|
while i < count
|
|
983
985
|
ptr = buffer.get_pointer(i * FFI::Pointer.size)
|
|
986
|
+
# Take ownership of this index before processing the pointer: advancing `i` up front means
|
|
987
|
+
# the cleanup loop in the `ensure` below starts past it, so a raise mid-iteration cannot
|
|
988
|
+
# double-free the same message.
|
|
989
|
+
i += 1
|
|
984
990
|
|
|
985
|
-
if ptr.null?
|
|
986
|
-
i += 1
|
|
987
|
-
next
|
|
988
|
-
end
|
|
991
|
+
next if ptr.null?
|
|
989
992
|
|
|
990
|
-
|
|
993
|
+
# One `ensure` around the whole body guarantees `ptr` is destroyed exactly once on every
|
|
994
|
+
# path - the error branch, a successful build, a rescued `RdkafkaError`, or any other
|
|
995
|
+
# exception unwinding through here - so nothing leaks in the window before the build.
|
|
996
|
+
begin
|
|
997
|
+
native_message = Rdkafka::Bindings::Message.new(ptr)
|
|
991
998
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
next
|
|
997
|
-
end
|
|
999
|
+
if native_message[:err] != Rdkafka::Bindings::RD_KAFKA_RESP_ERR_NO_ERROR
|
|
1000
|
+
results << build_batch_error(native_message)
|
|
1001
|
+
next
|
|
1002
|
+
end
|
|
998
1003
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1004
|
+
begin
|
|
1005
|
+
results << Rdkafka::Consumer::Message.new(native_message)
|
|
1006
|
+
rescue Rdkafka::RdkafkaError => e
|
|
1007
|
+
# A message that fails to build (e.g. a header read error) is surfaced inline as an
|
|
1008
|
+
# error event rather than discarding the whole batch - including the messages already
|
|
1009
|
+
# built - and raising, which silently lost them once their offsets had been stored.
|
|
1010
|
+
results << e
|
|
1011
|
+
end
|
|
1006
1012
|
ensure
|
|
1007
1013
|
Rdkafka::Bindings.rd_kafka_message_destroy(ptr)
|
|
1008
1014
|
end
|
|
1009
|
-
|
|
1010
|
-
i += 1
|
|
1011
1015
|
end
|
|
1012
1016
|
ensure
|
|
1013
1017
|
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: karafka-rdkafka
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.28.
|
|
4
|
+
version: 0.28.2
|
|
5
5
|
platform: aarch64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Thijs Cadier
|
|
@@ -141,8 +141,10 @@ files:
|
|
|
141
141
|
- lib/rdkafka/callbacks/incremental_alter_configs_handler.rb
|
|
142
142
|
- lib/rdkafka/callbacks/list_consumer_groups_handler.rb
|
|
143
143
|
- lib/rdkafka/callbacks/list_offsets_handler.rb
|
|
144
|
+
- lib/rdkafka/clients.rb
|
|
144
145
|
- lib/rdkafka/config.rb
|
|
145
146
|
- lib/rdkafka/consumer.rb
|
|
147
|
+
- lib/rdkafka/consumer/allocator.rb
|
|
146
148
|
- lib/rdkafka/consumer/headers.rb
|
|
147
149
|
- lib/rdkafka/consumer/message.rb
|
|
148
150
|
- lib/rdkafka/consumer/partition.rb
|