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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -8
  3. data/.github/workflows/push.yml +2 -2
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +16 -0
  6. data/Gemfile.lock +1 -1
  7. data/karafka-core.gemspec +1 -1
  8. data/lib/karafka/core/configurable/node.rb +8 -6
  9. data/lib/karafka/core/contractable/contract.rb +35 -9
  10. data/lib/karafka/core/contractable/result.rb +9 -0
  11. data/lib/karafka/core/instrumentation/callbacks_manager.rb +6 -1
  12. data/lib/karafka/core/monitoring/event.rb +21 -4
  13. data/lib/karafka/core/monitoring/notifications.rb +5 -16
  14. data/lib/karafka/core/monitoring/statistics_decorator.rb +192 -39
  15. data/lib/karafka/core/version.rb +1 -1
  16. metadata +2 -26
  17. data/test/lib/karafka/core/configurable/leaf_test.rb +0 -3
  18. data/test/lib/karafka/core/configurable/node_test.rb +0 -3
  19. data/test/lib/karafka/core/configurable_test.rb +0 -504
  20. data/test/lib/karafka/core/contractable/contract_test.rb +0 -241
  21. data/test/lib/karafka/core/contractable/result_test.rb +0 -106
  22. data/test/lib/karafka/core/contractable/rule_test.rb +0 -5
  23. data/test/lib/karafka/core/contractable_test.rb +0 -3
  24. data/test/lib/karafka/core/helpers/time_test.rb +0 -29
  25. data/test/lib/karafka/core/instrumentation/callbacks_manager_test.rb +0 -81
  26. data/test/lib/karafka/core/instrumentation_test.rb +0 -35
  27. data/test/lib/karafka/core/monitoring/event_test.rb +0 -25
  28. data/test/lib/karafka/core/monitoring/monitor_test.rb +0 -237
  29. data/test/lib/karafka/core/monitoring/notifications_test.rb +0 -275
  30. data/test/lib/karafka/core/monitoring/statistics_decorator_test.rb +0 -284
  31. data/test/lib/karafka/core/monitoring_test.rb +0 -3
  32. data/test/lib/karafka/core/patches/rdkafka/bindings_test.rb +0 -25
  33. data/test/lib/karafka/core/taggable/tags_test.rb +0 -66
  34. data/test/lib/karafka/core/taggable_test.rb +0 -36
  35. data/test/lib/karafka/core/version_test.rb +0 -5
  36. data/test/lib/karafka/core_test.rb +0 -13
  37. data/test/lib/karafka-core_test.rb +0 -3
  38. data/test/support/class_builder.rb +0 -24
  39. data/test/support/describe_current_helper.rb +0 -41
  40. 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 emited stats, so we get also
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
- def initialize(excluded_keys: [])
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 emited_stats [Hash] original emited statistics
45
- # @return [Hash] emited statistics extended with the diff data
46
- # @note We modify the emited statistics, instead of creating new. Since we don't expose
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(emited_stats)
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
- emited_stats,
56
- [],
57
- 0,
64
+ emitted_stats,
58
65
  change_d
59
66
  )
60
67
 
61
- @previous = emited_stats
68
+ @previous = emitted_stats
62
69
  @previous_at = current_at
63
70
 
64
- emited_stats.freeze
71
+ emitted_stats.freeze
65
72
  end
66
73
 
67
74
  private
68
75
 
69
- # Calculates the diff of the provided values, appends delta and freeze duration keys,
70
- # and modifies in place the emited statistics.
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
- # Uses `each_pair` with a per-call pending-writes buffer instead of `current.keys.each`
73
- # to avoid allocating a new Array for every Hash node in the statistics tree. At scale
74
- # (thousands of partitions), this reduces allocations from tens of thousands to one per call.
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
- # The append and suffix_keys_for logic is inlined to reduce method call overhead
77
- # (from ~915k method calls to ~39k at 6400 partitions).
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, pw, pw_start, change_d)
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.each_pair do |key, value|
115
+ current.keys.each do |key|
93
116
  next if excluded&.key?(key)
94
117
 
95
- if value.is_a?(Hash)
96
- diff(filled_previous[key], value, pw, pw_size, change_d)
97
- next
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
- pw[pw_size] = pair[0]
116
- pw[pw_size + 1] = (result == 0) ? (filled_previous[pair[0]] || 0) + change_d : 0
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
@@ -4,6 +4,6 @@ module Karafka
4
4
  module Core
5
5
  # Current Karafka::Core version
6
6
  # We follow the versioning schema of given Karafka version
7
- VERSION = "2.5.10"
7
+ VERSION = "2.5.12"
8
8
  end
9
9
  end
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.10
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.3
123
+ rubygems_version: 4.0.6
148
124
  specification_version: 4
149
125
  summary: Karafka ecosystem core modules
150
126
  test_files: []
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Tested in `configurable_spec.rb` via the use-cases.
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Tested in `configurable_spec.rb` via the use-cases.