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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe7a4974267a2a3b2b200e0763a13042d98695c0586931d00d0016087bc5dfe2
4
- data.tar.gz: a5bdd9235a999028ad120d79c2986e05986ee1b9df8a5de6b813fff634bc5619
3
+ metadata.gz: a1ca213972a7e05632f164e423383f6a9172926291140ad6005c822b347855b2
4
+ data.tar.gz: acb97b8887fed4e6534bed1c8c764a19a38462b6a6581893ed80b3bb1efd7a82
5
5
  SHA512:
6
- metadata.gz: be10ba6481782906bbb076ea69ab9aa05c80f4c295ce82c2faacc53ee27a8ebd01e4c7a200088205058fda7e7efef494d71ac96492d3cf074f579ea331597148
7
- data.tar.gz: a51a53327518e282ab42461fccc3aea78407de1418d97376a64b25dda687b616a54f0c6639b542d0c104534876b1c34d9f7f1434486300c40708f129f03ad267
6
+ metadata.gz: 0d3ead51c6a1fe7697cb5c13d914acd148c65d0be5b04be88b898d8cb5e220f65095bab181083a993a6085483c2f5815cb69c5cbd31cf92bcd246ea72c2f343a
7
+ data.tar.gz: 1210463f030fe15c691b0d136498327ae9a83128b5f871a256401bd498ebfd4117391d250ee49a1ab53c1f5f4fd427db292755bc792ea73a99435535bb79d115
@@ -14,7 +14,7 @@ permissions:
14
14
  contents: read
15
15
 
16
16
  jobs:
17
- specs:
17
+ tests:
18
18
  timeout-minutes: 15
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
@@ -37,7 +37,7 @@ jobs:
37
37
  run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
38
38
 
39
39
  - name: Set up Ruby
40
- uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
40
+ uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0
41
41
  with:
42
42
  ruby-version: ${{matrix.ruby}}
43
43
  bundler: 'latest'
@@ -69,9 +69,9 @@ jobs:
69
69
  with:
70
70
  fetch-depth: 0
71
71
  - name: Set up Ruby
72
- uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
72
+ uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0
73
73
  with:
74
- ruby-version: '4.0.1'
74
+ ruby-version: '4.0.2'
75
75
  bundler-cache: true
76
76
  - name: Run rubocop
77
77
  run: bundle exec rubocop
@@ -86,9 +86,9 @@ jobs:
86
86
  with:
87
87
  fetch-depth: 0
88
88
  - name: Set up Ruby
89
- uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
89
+ uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0
90
90
  with:
91
- ruby-version: '4.0.1'
91
+ ruby-version: '4.0.2'
92
92
  bundler-cache: true
93
93
  - name: Run yard-lint
94
94
  run: bundle exec yard-lint lib/
@@ -101,7 +101,7 @@ jobs:
101
101
  with:
102
102
  fetch-depth: 0
103
103
  - name: Set up Node.js
104
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
104
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
105
105
  with:
106
106
  node-version: '20'
107
107
  cache: 'npm'
@@ -116,7 +116,7 @@ jobs:
116
116
  if: always()
117
117
  needs:
118
118
  - rubocop
119
- - specs
119
+ - tests
120
120
  - yard-lint
121
121
  - lostconf
122
122
  steps:
@@ -24,7 +24,7 @@ jobs:
24
24
  fetch-depth: 0
25
25
 
26
26
  - name: Set up Ruby
27
- uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
27
+ uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0
28
28
  with:
29
29
  bundler-cache: false
30
30
 
@@ -32,4 +32,4 @@ jobs:
32
32
  run: |
33
33
  bundle install --jobs 4 --retry 3
34
34
 
35
- - uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2
35
+ - uses: rubygems/release-gem@e9a6361a0b14562539327c2a02373edc56dd3169 # v1.1.4
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.1
1
+ 4.0.2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Karafka Core Changelog
2
2
 
3
+ ## 2.5.12 (2026-04-02)
4
+ - [Fix] Exclude `test/` directory from gem releases to reduce package size.
5
+
6
+ ## 2.5.11 (2026-04-02)
7
+ - [Enhancement] Specialize `Contract#dig` for common 1-key and 2-key paths to avoid iterator overhead, yielding ~1.5x faster single-key lookups and ~1.45x faster two-key nested lookups.
8
+ - [Enhancement] Replace `Node#build_accessors` `@local_defs` Array with Hash for O(1) membership checks instead of O(n) `Array#include?`, yielding up to ~5x faster accessor lookups at 50 settings.
9
+ - [Enhancement] Use frozen `EMPTY_ARRAY` constant for `Contract#call` and `#validate!` default `scope` parameter to avoid allocating a new Array on every invocation, yielding ~1.36x faster call dispatch and saving 1 Array allocation per call.
10
+ - [Enhancement] Pre-resolve `@events_methods_map` method name before the listener notification loop in `Notifications#instrument` to avoid repeated Hash lookup per listener, yielding ~1.12x faster event dispatch with multiple listeners.
11
+ - [Enhancement] Cache a frozen success `Result` singleton via `Result.success` to eliminate 1 object allocation per successful `Contract#call` on the happy path.
12
+ - [Enhancement] Skip nestings block re-evaluation in `Node#deep_dup` to avoid recreating children that are immediately overwritten, yielding ~14x faster deep_dup and reducing allocations from ~620 to ~66 objects for large configs.
13
+ - [Enhancement] Cache `CallbacksManager#call` values snapshot and invalidate on `add`/`delete` to avoid allocating a new Array on every invocation while preserving thread-safety snapshot semantics, saving 1 Array allocation per call.
14
+ - [Enhancement] Store execution time separately in `Event` and build the merged payload hash lazily on `#payload` access, eliminating 1 Hash allocation per `Notifications#instrument` call when listeners use `#[]` access (the common pattern), yielding ~1.7x faster event dispatch.
15
+ - [Enhancement] Replace `StatisticsDecorator#diff` pending-writes buffer with `keys.each` direct-write iteration, eliminating the buffer and write-back loop for ~13% faster decoration at scale (10 brokers, 20 topics, 2000 partitions).
16
+ - [Enhancement] Reorder `StatisticsDecorator#diff` type checks to test `Numeric` before `Hash`, matching the ~80% numeric value distribution in librdkafka statistics.
17
+ - [Enhancement] Support `only_keys` option in `StatisticsDecorator` to decorate only specified numeric keys (e.g. `consumer_lag`, `committed_offset`). When combined with `excluded_keys`, reduces decoration cost from ~80ms to ~8.5ms per call on large clusters (10 brokers, 20 topics, 2000 partitions) by using structure-aware navigation of the librdkafka statistics tree and direct key access instead of full-hash iteration.
18
+
3
19
  ## 2.5.10 (2026-03-02)
4
20
  - [Enhancement] Introduce `MinitestLocator` helper for minitest/spec subject class auto-discovery from test file paths.
5
21
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- karafka-core (2.5.10)
4
+ karafka-core (2.5.12)
5
5
  karafka-rdkafka (>= 0.20.0)
6
6
  logger (>= 1.6.0)
7
7
 
data/karafka-core.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.required_ruby_version = ">= 3.2.0"
23
23
 
24
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test)/}) }
25
25
  spec.require_paths = %w[lib]
26
26
 
27
27
  spec.metadata = {
@@ -17,14 +17,16 @@ module Karafka
17
17
 
18
18
  # @param node_name [Symbol] node name
19
19
  # @param nestings [Proc] block for nested settings
20
- def initialize(node_name, nestings = ->(_) {})
20
+ # @param evaluate [Boolean] when false, skip evaluating the nestings block. Used by
21
+ # deep_dup to avoid re-creating children that will be overwritten immediately.
22
+ def initialize(node_name, nestings = ->(_) {}, evaluate: true)
21
23
  @node_name = node_name
22
24
  @children = []
23
25
  @nestings = nestings
24
26
  @compiled = false
25
27
  @configs_refs = {}
26
- @local_defs = []
27
- instance_eval(&nestings)
28
+ @local_defs = {}
29
+ instance_eval(&nestings) if evaluate
28
30
  end
29
31
 
30
32
  # Allows for a single leaf or nested node definition
@@ -83,7 +85,7 @@ module Karafka
83
85
  # and non-side-effect usage on an instance/inherited.
84
86
  # @return [Node] duplicated node
85
87
  def deep_dup
86
- dupped = Node.new(node_name, nestings)
88
+ dupped = Node.new(node_name, nestings, evaluate: false)
87
89
 
88
90
  children.each do |value|
89
91
  dupped.children << if value.is_a?(Leaf)
@@ -189,8 +191,8 @@ module Karafka
189
191
  # object and then we would not redefine it for nodes. This ensures that we only do not
190
192
  # redefine our own definitions but we do redefine any user "accidentally" inherited
191
193
  # methods
192
- if reader_respond ? !@local_defs.include?(reader_name) : true
193
- @local_defs << reader_name
194
+ if reader_respond ? !@local_defs.key?(reader_name) : true
195
+ @local_defs[reader_name] = true
194
196
 
195
197
  define_singleton_method(reader_name) do
196
198
  @configs_refs[reader_name]
@@ -14,7 +14,12 @@ module Karafka
14
14
  # prevent additional array allocation
15
15
  DIG_MISS = Object.new
16
16
 
17
- private_constant :DIG_MISS
17
+ # Empty array for scope default to avoid allocating a new Array on each
18
+ # `#call` / `#validate!` invocation. Safe because scope is never mutated – it is only
19
+ # used in `scope + rule.path` which creates a new Array.
20
+ EMPTY_ARRAY = [].freeze
21
+
22
+ private_constant :DIG_MISS, :EMPTY_ARRAY
18
23
 
19
24
  # Yaml based error messages data
20
25
  setting(:error_messages)
@@ -80,7 +85,7 @@ module Karafka
80
85
  # @param scope [Array<String>] scope of this contract (if any) or empty array if no parent
81
86
  # scope is needed if contract starts from root
82
87
  # @return [Result] validaton result
83
- def call(data, scope: [])
88
+ def call(data, scope: EMPTY_ARRAY)
84
89
  errors = []
85
90
 
86
91
  self.class.rules.each do |rule|
@@ -94,6 +99,8 @@ module Karafka
94
99
  end
95
100
  end
96
101
 
102
+ return Result.success if errors.empty?
103
+
97
104
  Result.new(errors, self)
98
105
  end
99
106
 
@@ -104,7 +111,7 @@ module Karafka
104
111
  # @return [Boolean] true
105
112
  # @raise [StandardError] any error provided in the error_class that inherits from the
106
113
  # standard error
107
- def validate!(data, error_class, scope: [])
114
+ def validate!(data, error_class, scope: EMPTY_ARRAY)
108
115
  result = call(data, scope: scope)
109
116
 
110
117
  return true if result.success?
@@ -183,15 +190,34 @@ module Karafka
183
190
  # @param keys [Array<Symbol>]
184
191
  # @return [DIG_MISS, Object] found element or DIGG_MISS indicating that not found
185
192
  def dig(data, keys)
186
- current = data
193
+ case keys.length
194
+ when 1
195
+ key = keys[0]
187
196
 
188
- keys.each do |nesting|
189
- return DIG_MISS unless current.key?(nesting)
197
+ return DIG_MISS unless data.key?(key)
190
198
 
191
- current = current[nesting]
192
- end
199
+ data[key]
200
+ when 2
201
+ key1 = keys[0]
202
+
203
+ return DIG_MISS unless data.key?(key1)
204
+
205
+ mid = data[key1]
206
+
207
+ return DIG_MISS unless mid.is_a?(Hash) && mid.key?(keys[1])
193
208
 
194
- current
209
+ mid[keys[1]]
210
+ else
211
+ current = data
212
+
213
+ keys.each do |nesting|
214
+ return DIG_MISS unless current.key?(nesting)
215
+
216
+ current = current[nesting]
217
+ end
218
+
219
+ current
220
+ end
195
221
  end
196
222
  end
197
223
  end
@@ -12,6 +12,15 @@ module Karafka
12
12
 
13
13
  private_constant :EMPTY_HASH
14
14
 
15
+ class << self
16
+ # @return [Result] cached frozen success result to avoid allocating a new Result on
17
+ # every successful contract validation. Safe because successful results are
18
+ # identical regardless of contract (they all have @errors = EMPTY_HASH).
19
+ def success
20
+ @success ||= new([], nil).freeze
21
+ end
22
+ end
23
+
15
24
  # Builds a result object and remaps (if needed) error keys to proper error messages
16
25
  #
17
26
  # @param errors [Array<Array>] array with sub-arrays with paths and error keys
@@ -10,6 +10,7 @@ module Karafka
10
10
  # @return [::Karafka::Core::Instrumentation::CallbacksManager]
11
11
  def initialize
12
12
  @callbacks = {}
13
+ @values_cache = nil
13
14
  end
14
15
 
15
16
  # Invokes all the callbacks registered one after another
@@ -19,8 +20,10 @@ module Karafka
19
20
  # callbacks and add new at the same time. Since we don't know when and in what thread
20
21
  # things are going to be added to the manager, we need to extract values into an array and
21
22
  # run it. That way we can add new things the same time.
23
+ # The values snapshot is cached and invalidated on add/delete to avoid allocating a new
24
+ # Array on every call while preserving the thread-safety snapshot semantics.
22
25
  def call(*args)
23
- @callbacks.values.each { |callback| callback.call(*args) }
26
+ (@values_cache ||= @callbacks.values).each { |callback| callback.call(*args) }
24
27
  end
25
28
 
26
29
  # Adds a callback to the manager
@@ -29,12 +32,14 @@ module Karafka
29
32
  # @param callable [#call] object that responds to a `#call` method
30
33
  def add(id, callable)
31
34
  @callbacks[id] = callable
35
+ @values_cache = nil
32
36
  end
33
37
 
34
38
  # Removes the callback from the manager
35
39
  # @param id [String] id of the callback we want to remove
36
40
  def delete(id)
37
41
  @callbacks.delete(id)
42
+ @values_cache = nil
38
43
  end
39
44
  end
40
45
  end
@@ -5,20 +5,37 @@ module Karafka
5
5
  module Monitoring
6
6
  # Single notification event wrapping payload with id
7
7
  class Event
8
- attr_reader :id, :payload
8
+ attr_reader :id
9
9
 
10
10
  # @param id [String, Symbol] id of the event
11
11
  # @param payload [Hash] event payload
12
- def initialize(id, payload)
12
+ # @param time [Float, nil] execution time, stored separately to avoid eager hash
13
+ # allocation. Merged into the payload lazily only when `#payload` is accessed.
14
+ def initialize(id, payload, time = nil)
13
15
  @id = id
14
- @payload = payload
16
+ @raw_payload = payload
17
+ @time = time
18
+ @payload = nil
19
+ end
20
+
21
+ # @return [Hash] full payload including time (if set). The merged hash is built lazily
22
+ # on first access to avoid allocating a new Hash when listeners only use `#[]`.
23
+ def payload
24
+ @payload ||= if @time
25
+ @raw_payload.empty? ? { time: @time } : @raw_payload.merge(time: @time)
26
+ else
27
+ @raw_payload
28
+ end
15
29
  end
16
30
 
17
31
  # Hash access to the payload data (if present)
32
+ # Provides direct access to time without triggering payload hash construction.
18
33
  #
19
34
  # @param name [String, Symbol]
20
35
  def [](name)
21
- @payload.fetch(name)
36
+ return @time if name == :time && @time
37
+
38
+ @raw_payload.fetch(name)
22
39
  end
23
40
  end
24
41
  end
@@ -139,36 +139,25 @@ module Karafka
139
139
  return
140
140
  end
141
141
 
142
- final_payload = build_payload(payload, time)
143
- event = Event.new(event_id, final_payload)
142
+ event = Event.new(event_id, payload, time)
144
143
 
145
- notify_listeners(event_id, event, assigned_listeners)
144
+ notify_listeners(@events_methods_map[event_id], event, assigned_listeners)
146
145
 
147
146
  result
148
147
  end
149
148
 
150
149
  private
151
150
 
152
- # Builds the final payload with time information if available
153
- # @param payload [Hash] original payload
154
- # @param time [Float, nil] execution time if block was given
155
- # @return [Hash] final payload
156
- def build_payload(payload, time)
157
- return payload unless time
158
-
159
- payload.empty? ? { time: time } : payload.merge(time: time)
160
- end
161
-
162
151
  # Notifies all assigned listeners about the event
163
- # @param event_id [String]
152
+ # @param method_name [Symbol] pre-resolved method name for object listeners
164
153
  # @param event [Event] event with payload to broadcast to listeners
165
154
  # @param assigned_listeners [Array] list of listeners to notify
166
- def notify_listeners(event_id, event, assigned_listeners)
155
+ def notify_listeners(method_name, event, assigned_listeners)
167
156
  assigned_listeners.each do |listener|
168
157
  if listener.is_a?(Proc)
169
158
  listener.call(event)
170
159
  else
171
- listener.send(@events_methods_map[event_id], event)
160
+ listener.send(method_name, event)
172
161
  end
173
162
  end
174
163
  end