karafka-core 2.5.10 → 2.5.12
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/.github/workflows/ci.yml +8 -8
- data/.github/workflows/push.yml +2 -2
- data/.ruby-version +1 -1
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +1 -1
- data/karafka-core.gemspec +1 -1
- data/lib/karafka/core/configurable/node.rb +8 -6
- data/lib/karafka/core/contractable/contract.rb +35 -9
- data/lib/karafka/core/contractable/result.rb +9 -0
- data/lib/karafka/core/instrumentation/callbacks_manager.rb +6 -1
- data/lib/karafka/core/monitoring/event.rb +21 -4
- data/lib/karafka/core/monitoring/notifications.rb +5 -16
- data/lib/karafka/core/monitoring/statistics_decorator.rb +192 -39
- data/lib/karafka/core/version.rb +1 -1
- metadata +2 -26
- data/test/lib/karafka/core/configurable/leaf_test.rb +0 -3
- data/test/lib/karafka/core/configurable/node_test.rb +0 -3
- data/test/lib/karafka/core/configurable_test.rb +0 -504
- data/test/lib/karafka/core/contractable/contract_test.rb +0 -241
- data/test/lib/karafka/core/contractable/result_test.rb +0 -106
- data/test/lib/karafka/core/contractable/rule_test.rb +0 -5
- data/test/lib/karafka/core/contractable_test.rb +0 -3
- data/test/lib/karafka/core/helpers/time_test.rb +0 -29
- data/test/lib/karafka/core/instrumentation/callbacks_manager_test.rb +0 -81
- data/test/lib/karafka/core/instrumentation_test.rb +0 -35
- data/test/lib/karafka/core/monitoring/event_test.rb +0 -25
- data/test/lib/karafka/core/monitoring/monitor_test.rb +0 -237
- data/test/lib/karafka/core/monitoring/notifications_test.rb +0 -275
- data/test/lib/karafka/core/monitoring/statistics_decorator_test.rb +0 -284
- data/test/lib/karafka/core/monitoring_test.rb +0 -3
- data/test/lib/karafka/core/patches/rdkafka/bindings_test.rb +0 -25
- data/test/lib/karafka/core/taggable/tags_test.rb +0 -66
- data/test/lib/karafka/core/taggable_test.rb +0 -36
- data/test/lib/karafka/core/version_test.rb +0 -5
- data/test/lib/karafka/core_test.rb +0 -13
- data/test/lib/karafka-core_test.rb +0 -3
- data/test/support/class_builder.rb +0 -24
- data/test/support/describe_current_helper.rb +0 -41
- data/test/test_helper.rb +0 -55
|
@@ -6,7 +6,7 @@ module Karafka
|
|
|
6
6
|
# Many of the librdkafka statistics are absolute values instead of a gauge.
|
|
7
7
|
# This means, that for example number of messages sent is an absolute growing value
|
|
8
8
|
# instead of being a value of messages sent from the last statistics report.
|
|
9
|
-
# This decorator calculates the diff against previously
|
|
9
|
+
# This decorator calculates the diff against previously emitted stats, so we get also
|
|
10
10
|
# the diff together with the original values
|
|
11
11
|
#
|
|
12
12
|
# It adds two extra values to numerics:
|
|
@@ -28,7 +28,12 @@ module Karafka
|
|
|
28
28
|
# duration suffixes. This is useful for skipping large subtrees of the librdkafka
|
|
29
29
|
# statistics that are not consumed by the application (e.g. broker toppars, window
|
|
30
30
|
# stats like int_latency, outbuf_latency, throttle, batchsize, batchcnt, req).
|
|
31
|
-
|
|
31
|
+
# @param only_keys [Array<String>] when set, only these numeric keys will be decorated
|
|
32
|
+
# with delta/freeze duration suffixes. Hash children are still recursed into for
|
|
33
|
+
# structural navigation, but only the listed keys receive _d and _fd computation.
|
|
34
|
+
# This drastically reduces work at the partition level where most cost occurs.
|
|
35
|
+
# When empty (default), all numeric keys are decorated.
|
|
36
|
+
def initialize(excluded_keys: [], only_keys: [])
|
|
32
37
|
@previous = EMPTY_HASH
|
|
33
38
|
# Operate on ms precision only
|
|
34
39
|
@previous_at = monotonic_now.round
|
|
@@ -39,63 +44,222 @@ module Karafka
|
|
|
39
44
|
@excluded_keys = unless excluded_keys.empty?
|
|
40
45
|
excluded_keys.each_with_object({}) { |k, h| h[k] = true }.freeze
|
|
41
46
|
end
|
|
47
|
+
# Frozen array for direct-access decoration, nil when empty to use full decoration
|
|
48
|
+
@only_keys = unless only_keys.empty?
|
|
49
|
+
only_keys.freeze
|
|
50
|
+
end
|
|
42
51
|
end
|
|
43
52
|
|
|
44
|
-
# @param
|
|
45
|
-
# @return [Hash]
|
|
46
|
-
# @note We modify the
|
|
53
|
+
# @param emitted_stats [Hash] original emitted statistics
|
|
54
|
+
# @return [Hash] emitted statistics extended with the diff data
|
|
55
|
+
# @note We modify the emitted statistics, instead of creating new. Since we don't expose
|
|
47
56
|
# any API to get raw data, users can just assume that the result of this decoration is
|
|
48
57
|
# the proper raw stats that they can use
|
|
49
|
-
def call(
|
|
58
|
+
def call(emitted_stats)
|
|
50
59
|
current_at = monotonic_now.round
|
|
51
60
|
change_d = current_at - @previous_at
|
|
52
61
|
|
|
53
62
|
diff(
|
|
54
63
|
@previous,
|
|
55
|
-
|
|
56
|
-
[],
|
|
57
|
-
0,
|
|
64
|
+
emitted_stats,
|
|
58
65
|
change_d
|
|
59
66
|
)
|
|
60
67
|
|
|
61
|
-
@previous =
|
|
68
|
+
@previous = emitted_stats
|
|
62
69
|
@previous_at = current_at
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
emitted_stats.freeze
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
private
|
|
68
75
|
|
|
69
|
-
# Calculates the diff of the provided values
|
|
70
|
-
#
|
|
76
|
+
# Calculates the diff of the provided values and modifies the emitted statistics
|
|
77
|
+
# in place to add delta and freeze duration keys.
|
|
71
78
|
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
79
|
+
# When @only_keys is set, uses a two-phase approach: first recurse into hash
|
|
80
|
+
# children for structural navigation, then decorate only the specified keys via
|
|
81
|
+
# direct hash access (no iteration over non-target keys). This drastically reduces
|
|
82
|
+
# work at the partition level where most of the cost occurs.
|
|
75
83
|
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
84
|
+
# When @only_keys is nil, uses full decoration: iterates all keys, decorating every
|
|
85
|
+
# numeric value found.
|
|
78
86
|
#
|
|
79
87
|
# @param previous [Object] previous value from the given scope in which we are
|
|
80
88
|
# @param current [Object] current scope from emitted statistics
|
|
81
|
-
# @param pw [Array] pending writes buffer shared across recursive calls
|
|
82
|
-
# @param pw_start [Integer] starting offset in the buffer for this hash level
|
|
83
89
|
# @param change_d [Integer] time delta in ms since last stats emission
|
|
84
|
-
def diff(previous, current,
|
|
90
|
+
def diff(previous, current, change_d)
|
|
85
91
|
return unless current.is_a?(Hash)
|
|
86
92
|
|
|
93
|
+
if @only_keys
|
|
94
|
+
diff_only_keys(previous, current, change_d)
|
|
95
|
+
else
|
|
96
|
+
diff_all(previous, current, change_d)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Full decoration path: iterates all keys, decorating every numeric value.
|
|
101
|
+
#
|
|
102
|
+
# Uses `keys.each` to snapshot the current hash's key list, allowing direct writes
|
|
103
|
+
# to the hash during iteration without a pending-writes buffer.
|
|
104
|
+
#
|
|
105
|
+
# Checks Numeric before Hash because ~80% of statistics values are numeric.
|
|
106
|
+
#
|
|
107
|
+
# @param previous [Object] previous value from the given scope
|
|
108
|
+
# @param current [Hash] current scope from emitted statistics
|
|
109
|
+
# @param change_d [Integer] time delta in ms
|
|
110
|
+
def diff_all(previous, current, change_d)
|
|
87
111
|
filled_previous = previous || EMPTY_HASH
|
|
88
112
|
cache = @suffix_keys_cache
|
|
89
113
|
excluded = @excluded_keys
|
|
90
|
-
pw_size = pw_start
|
|
91
114
|
|
|
92
|
-
current.
|
|
115
|
+
current.keys.each do |key|
|
|
93
116
|
next if excluded&.key?(key)
|
|
94
117
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
value = current[key]
|
|
119
|
+
|
|
120
|
+
if value.is_a?(Numeric)
|
|
121
|
+
prev_value = filled_previous[key]
|
|
122
|
+
|
|
123
|
+
if prev_value.nil?
|
|
124
|
+
result = 0
|
|
125
|
+
elsif prev_value.is_a?(Numeric)
|
|
126
|
+
result = value - prev_value
|
|
127
|
+
else
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
pair = cache[key] || (cache[key] = ["#{key}_fd".freeze, "#{key}_d".freeze].freeze)
|
|
132
|
+
|
|
133
|
+
current[pair[0]] = (result == 0) ? (filled_previous[pair[0]] || 0) + change_d : 0
|
|
134
|
+
current[pair[1]] = result
|
|
135
|
+
elsif value.is_a?(Hash)
|
|
136
|
+
diff_all(filled_previous[key], value, change_d)
|
|
98
137
|
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Known librdkafka statistics tree structural keys. Used by diff_only_keys to
|
|
142
|
+
# navigate directly to known hash paths instead of iterating all keys.
|
|
143
|
+
KNOWN_HASH_KEYS = { "brokers" => true, "topics" => true, "cgrp" => true }.freeze
|
|
144
|
+
|
|
145
|
+
# Known structural keys within a topic hash
|
|
146
|
+
TOPIC_HASH_KEYS = { "partitions" => true }.freeze
|
|
147
|
+
|
|
148
|
+
private_constant :KNOWN_HASH_KEYS, :TOPIC_HASH_KEYS
|
|
149
|
+
|
|
150
|
+
# Selective decoration path: navigates known librdkafka statistics tree structure
|
|
151
|
+
# directly instead of iterating all keys to find hash children. Decorates only the
|
|
152
|
+
# specified keys via direct access at each level.
|
|
153
|
+
#
|
|
154
|
+
# This eliminates the generic recursion overhead (72,000+ is_a?(Hash) checks at the
|
|
155
|
+
# partition level alone on a 2000-partition cluster) by leveraging knowledge of the
|
|
156
|
+
# librdkafka statistics layout: root → brokers → broker, root → topics → topic →
|
|
157
|
+
# partitions → partition, root → cgrp.
|
|
158
|
+
#
|
|
159
|
+
# For non-librdkafka hash children (e.g. custom or test data), falls back to generic
|
|
160
|
+
# recursion to maintain correctness with arbitrary nested structures.
|
|
161
|
+
#
|
|
162
|
+
# @param previous [Object] previous value from the given scope
|
|
163
|
+
# @param current [Hash] current stats hash (root level)
|
|
164
|
+
# @param change_d [Integer] time delta in ms
|
|
165
|
+
def diff_only_keys(previous, current, change_d)
|
|
166
|
+
filled_previous = previous || EMPTY_HASH
|
|
167
|
+
excluded = @excluded_keys
|
|
168
|
+
|
|
169
|
+
# Root level decoration
|
|
170
|
+
decorate_keys(current, filled_previous, change_d)
|
|
171
|
+
|
|
172
|
+
# Brokers: each child is a broker hash (leaf-like for only_keys)
|
|
173
|
+
unless excluded&.key?("brokers")
|
|
174
|
+
brokers = current["brokers"]
|
|
175
|
+
|
|
176
|
+
if brokers.is_a?(Hash)
|
|
177
|
+
prev_brokers = filled_previous["brokers"] || EMPTY_HASH
|
|
178
|
+
|
|
179
|
+
brokers.each_pair do |name, broker|
|
|
180
|
+
decorate_keys(broker, prev_brokers[name] || EMPTY_HASH, change_d) if broker.is_a?(Hash)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Topics → Partitions
|
|
186
|
+
unless excluded&.key?("topics")
|
|
187
|
+
topics = current["topics"]
|
|
188
|
+
|
|
189
|
+
if topics.is_a?(Hash)
|
|
190
|
+
prev_topics = filled_previous["topics"] || EMPTY_HASH
|
|
191
|
+
|
|
192
|
+
topics.each_pair do |name, topic|
|
|
193
|
+
next unless topic.is_a?(Hash)
|
|
194
|
+
|
|
195
|
+
prev_topic = prev_topics[name] || EMPTY_HASH
|
|
196
|
+
decorate_keys(topic, prev_topic, change_d)
|
|
197
|
+
|
|
198
|
+
unless excluded&.key?("partitions")
|
|
199
|
+
partitions = topic["partitions"]
|
|
200
|
+
|
|
201
|
+
if partitions.is_a?(Hash)
|
|
202
|
+
prev_partitions = prev_topic["partitions"] || EMPTY_HASH
|
|
203
|
+
|
|
204
|
+
partitions.each_pair do |pid, partition|
|
|
205
|
+
# Leaf level: no hash children, just decorate
|
|
206
|
+
decorate_keys(partition, prev_partitions[pid] || EMPTY_HASH, change_d)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Consumer group (leaf-like)
|
|
215
|
+
cgrp = current["cgrp"]
|
|
216
|
+
decorate_keys(cgrp, filled_previous["cgrp"] || EMPTY_HASH, change_d) if cgrp.is_a?(Hash)
|
|
217
|
+
|
|
218
|
+
# Fallback: handle any non-standard hash children not in the known structure.
|
|
219
|
+
# This ensures correctness for arbitrary nested data while the known paths above
|
|
220
|
+
# provide the fast path for real librdkafka statistics.
|
|
221
|
+
current.each_pair do |key, value|
|
|
222
|
+
next if KNOWN_HASH_KEYS.key?(key)
|
|
223
|
+
next if excluded&.key?(key)
|
|
224
|
+
next unless value.is_a?(Hash)
|
|
225
|
+
|
|
226
|
+
diff_only_keys_generic(filled_previous[key], value, change_d)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Generic recursive fallback for only_keys mode on non-standard hash children.
|
|
231
|
+
# Used for hash subtrees not covered by the known librdkafka structure.
|
|
232
|
+
#
|
|
233
|
+
# @param previous [Object] previous value
|
|
234
|
+
# @param current [Hash] current hash to process
|
|
235
|
+
# @param change_d [Integer] time delta in ms
|
|
236
|
+
def diff_only_keys_generic(previous, current, change_d)
|
|
237
|
+
return unless current.is_a?(Hash)
|
|
238
|
+
|
|
239
|
+
filled_previous = previous || EMPTY_HASH
|
|
240
|
+
excluded = @excluded_keys
|
|
241
|
+
|
|
242
|
+
decorate_keys(current, filled_previous, change_d)
|
|
243
|
+
|
|
244
|
+
current.each_pair do |key, value|
|
|
245
|
+
next if excluded&.key?(key)
|
|
246
|
+
|
|
247
|
+
diff_only_keys_generic(filled_previous[key], value, change_d) if value.is_a?(Hash)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Decorates only the keys listed in @only_keys via direct hash access.
|
|
252
|
+
# No iteration over the full hash, no type checking on non-target keys.
|
|
253
|
+
#
|
|
254
|
+
# @param current [Hash] current stats node
|
|
255
|
+
# @param filled_previous [Hash] previous stats node
|
|
256
|
+
# @param change_d [Integer] time delta in ms
|
|
257
|
+
def decorate_keys(current, filled_previous, change_d)
|
|
258
|
+
cache = @suffix_keys_cache
|
|
259
|
+
only = @only_keys
|
|
260
|
+
|
|
261
|
+
only.each do |key|
|
|
262
|
+
value = current[key]
|
|
99
263
|
|
|
100
264
|
next unless value.is_a?(Numeric)
|
|
101
265
|
|
|
@@ -109,21 +273,10 @@ module Karafka
|
|
|
109
273
|
next
|
|
110
274
|
end
|
|
111
275
|
|
|
112
|
-
# Inlined suffix_keys_for for reduced method call overhead
|
|
113
276
|
pair = cache[key] || (cache[key] = ["#{key}_fd".freeze, "#{key}_d".freeze].freeze)
|
|
114
277
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
pw[pw_size + 2] = pair[1]
|
|
118
|
-
pw[pw_size + 3] = result
|
|
119
|
-
pw_size += 4
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Apply collected writes for this hash level
|
|
123
|
-
i = pw_start
|
|
124
|
-
while i < pw_size
|
|
125
|
-
current[pw[i]] = pw[i + 1]
|
|
126
|
-
i += 2
|
|
278
|
+
current[pair[0]] = (result == 0) ? (filled_previous[pair[0]] || 0) + change_d : 0
|
|
279
|
+
current[pair[1]] = result
|
|
127
280
|
end
|
|
128
281
|
end
|
|
129
282
|
end
|
data/lib/karafka/core/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: karafka-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.5.
|
|
4
|
+
version: 2.5.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maciej Mensfeld
|
|
@@ -96,30 +96,6 @@ files:
|
|
|
96
96
|
- package-lock.json
|
|
97
97
|
- package.json
|
|
98
98
|
- renovate.json
|
|
99
|
-
- test/lib/karafka-core_test.rb
|
|
100
|
-
- test/lib/karafka/core/configurable/leaf_test.rb
|
|
101
|
-
- test/lib/karafka/core/configurable/node_test.rb
|
|
102
|
-
- test/lib/karafka/core/configurable_test.rb
|
|
103
|
-
- test/lib/karafka/core/contractable/contract_test.rb
|
|
104
|
-
- test/lib/karafka/core/contractable/result_test.rb
|
|
105
|
-
- test/lib/karafka/core/contractable/rule_test.rb
|
|
106
|
-
- test/lib/karafka/core/contractable_test.rb
|
|
107
|
-
- test/lib/karafka/core/helpers/time_test.rb
|
|
108
|
-
- test/lib/karafka/core/instrumentation/callbacks_manager_test.rb
|
|
109
|
-
- test/lib/karafka/core/instrumentation_test.rb
|
|
110
|
-
- test/lib/karafka/core/monitoring/event_test.rb
|
|
111
|
-
- test/lib/karafka/core/monitoring/monitor_test.rb
|
|
112
|
-
- test/lib/karafka/core/monitoring/notifications_test.rb
|
|
113
|
-
- test/lib/karafka/core/monitoring/statistics_decorator_test.rb
|
|
114
|
-
- test/lib/karafka/core/monitoring_test.rb
|
|
115
|
-
- test/lib/karafka/core/patches/rdkafka/bindings_test.rb
|
|
116
|
-
- test/lib/karafka/core/taggable/tags_test.rb
|
|
117
|
-
- test/lib/karafka/core/taggable_test.rb
|
|
118
|
-
- test/lib/karafka/core/version_test.rb
|
|
119
|
-
- test/lib/karafka/core_test.rb
|
|
120
|
-
- test/support/class_builder.rb
|
|
121
|
-
- test/support/describe_current_helper.rb
|
|
122
|
-
- test/test_helper.rb
|
|
123
99
|
homepage: https://karafka.io
|
|
124
100
|
licenses:
|
|
125
101
|
- MIT
|
|
@@ -144,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
144
120
|
- !ruby/object:Gem::Version
|
|
145
121
|
version: '0'
|
|
146
122
|
requirements: []
|
|
147
|
-
rubygems_version: 4.0.
|
|
123
|
+
rubygems_version: 4.0.6
|
|
148
124
|
specification_version: 4
|
|
149
125
|
summary: Karafka ecosystem core modules
|
|
150
126
|
test_files: []
|