karafka-core 2.5.13 → 2.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b7b0d02f8480d00419f928a4fd78f6159e4d7fdb24cae7d426141c7b38c5eb1
4
- data.tar.gz: 45077c9e98c506d1f2e40c82b81548bebf7105ba606dedda2cc2a3eae43a0679
3
+ metadata.gz: 2d9f6ba569f0c3a911cbe3ce80c545f3069b5aa2560782cf88bd11c97baa0432
4
+ data.tar.gz: 54f274da23e99841809f884ea871ce9a55705d76d3c4e5c40b0ef2ffbe0ae75a
5
5
  SHA512:
6
- metadata.gz: faeb1f7ceed771456e3b6c2864042efe95d1a398969987873d8fe9773d8bda46bf2f16fddc4e4da9dced9f96d1fa17de3924cd0723e3f4a7f92c31d82be00b13
7
- data.tar.gz: 064c8be576d37001c06cb13d6694b1e044662c76867a625db52a9551dcc00cc0f2be67cc01497979be3025bc837e0282e596c27949bc9f8d085dca939c46d961
6
+ metadata.gz: 3a6c6ba64216479cebb2c4f574dff194ae6665189f382b0a60193c5b4ec3ab4d55f4ff97d7fb3fc90800f105a5284da27493b5694adff6e1a6c0738fa5795f65
7
+ data.tar.gz: 8ea0eddcd9ae95603041f8a74a1a26311cd9aa13744759dd6e7a699eb6ce682fd89596ef85e77ee2fd280d4aa3eca14d8f2ddd7a75dc322fe6e7bb33903ac7b6
@@ -29,7 +29,7 @@ jobs:
29
29
  - ruby: '4.0'
30
30
  coverage: 'true'
31
31
  steps:
32
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
32
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
33
33
  with:
34
34
  fetch-depth: 0
35
35
 
@@ -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@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
40
+ uses: ruby/setup-ruby@12fd324f1d0b43274fdc8130f6980590a667c455 # v1.312.0
41
41
  with:
42
42
  ruby-version: ${{matrix.ruby}}
43
43
  bundler: 'latest'
@@ -65,13 +65,13 @@ jobs:
65
65
  env:
66
66
  BUNDLE_GEMFILE: Gemfile.lint
67
67
  steps:
68
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
68
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
69
69
  with:
70
70
  fetch-depth: 0
71
71
  - name: Set up Ruby
72
- uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
72
+ uses: ruby/setup-ruby@12fd324f1d0b43274fdc8130f6980590a667c455 # v1.312.0
73
73
  with:
74
- ruby-version: '4.0.2'
74
+ ruby-version: '4.0.5'
75
75
  bundler-cache: true
76
76
  - name: Run rubocop
77
77
  run: bundle exec rubocop
@@ -82,13 +82,13 @@ jobs:
82
82
  env:
83
83
  BUNDLE_GEMFILE: Gemfile.lint
84
84
  steps:
85
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
85
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
86
86
  with:
87
87
  fetch-depth: 0
88
88
  - name: Set up Ruby
89
- uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
89
+ uses: ruby/setup-ruby@12fd324f1d0b43274fdc8130f6980590a667c455 # v1.312.0
90
90
  with:
91
- ruby-version: '4.0.2'
91
+ ruby-version: '4.0.5'
92
92
  bundler-cache: true
93
93
  - name: Run yard-lint
94
94
  run: bundle exec yard-lint lib/
@@ -97,11 +97,11 @@ jobs:
97
97
  timeout-minutes: 5
98
98
  runs-on: ubuntu-latest
99
99
  steps:
100
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
100
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
101
101
  with:
102
102
  fetch-depth: 0
103
103
  - name: Set up Node.js
104
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
104
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
105
105
  with:
106
106
  node-version: '20'
107
107
  cache: 'npm'
@@ -19,12 +19,12 @@ jobs:
19
19
  id-token: write
20
20
 
21
21
  steps:
22
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
22
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
23
23
  with:
24
24
  fetch-depth: 0
25
25
 
26
26
  - name: Set up Ruby
27
- uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
27
+ uses: ruby/setup-ruby@12fd324f1d0b43274fdc8130f6980590a667c455 # v1.312.0
28
28
  with:
29
29
  bundler-cache: false
30
30
 
@@ -7,7 +7,7 @@ jobs:
7
7
  verify_action_pins:
8
8
  runs-on: ubuntu-latest
9
9
  steps:
10
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
10
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
11
11
  - name: Check SHA pins
12
12
  run: |
13
13
  if grep -E -r "uses: .*/.*@(v[0-9]+|main|master)($|[[:space:]]|$)" --include="*.yml" --include="*.yaml" .github/workflows/ | grep -v "#"; then
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.2
1
+ 4.0.5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Karafka Core Changelog
2
2
 
3
+ ## 2.6.1 (2026-06-15)
4
+ - [Enhancement] Speed up `Contract#call` by ~1.25x for minimal and ~1.4x for fully populated data: resolve rule paths with a single `Hash#fetch` per level instead of `key?` + `[]`, inline the per-rule type dispatch into the rules loop, and compare the dig sentinel via `#equal?` so `#==` is never dispatched to the validated (user-provided) values. This is the per-message validation path in WaterDrop producers.
5
+ - [Fix] `Contract#call` with rule paths of 3+ keys no longer raises `NoMethodError` when an intermediate value is not a `Hash` and reports the path as missing instead, consistent with the 2-key path behavior.
6
+ - [Change] Reject reserved setting names with an `ArgumentError` in `Configurable::Node#setting` and `#register`: internal state names (`node_name`, `children`, `nestings`, `compiled`, `configs_refs`, `local_defs`) and the node public API names (`setting`, `configure`, `to_h`, `deep_dup`, `register`, `compile`). Previously such names silently shadowed the node own accessors, breaking `deep_dup` or `to_h`, and assignments like `config.children = value` corrupted the node internal state.
7
+ - [Enhancement] Skip the event name mapping hash lookup in `Monitor#instrument` when no namespace is used and the event id is already a `String`, which is the case for all events in the Karafka ecosystem (~1.2x faster dispatch on the common no-subscribers path). Symbol event ids and namespaced monitors keep going through the mapping.
8
+ - [Enhancement] Mirror config values into instance variables and use `attr_reader` based readers in `Configurable::Node`, yielding ~1.4x faster flat and ~1.6x faster nested settings reads on hot paths. `@configs_refs` remains the canonical store; non-identifier setting names (e.g. registered names with dashes) keep the previous hash-based accessors.
9
+ - [Enhancement] Instantiate each `Configurable::Node` through a per-layout anonymous subclass so the ivar-backed settings do not grow object shape variations on the shared `Node` class (which would degrade ivar access and trigger Ruby performance warnings). `deep_dup` reuses the template's subclass, so duplicated configs share object shapes.
10
+ - [Fix] Symbolize setting names at definition time (`setting`, same as `register`) and on config store writes so `String` setting names work end to end (accessors, `#to_h`, recompilation state) and cannot corrupt node internal state when matching reserved internal names (previously string-named settings were quietly broken as accessors and the store disagreed on the key type).
11
+ - [Change] Config nodes are now instances of anonymous `Node` subclasses: `is_a?(Karafka::Core::Configurable::Node)` still holds, but `instance_of?(Node)` is now `false` and `node.class.name` is `nil`.
12
+ - [Change] Assigning a setting on a frozen config node now raises `FrozenError` (previously the write silently mutated internal storage despite the freeze).
13
+
14
+ ## 2.6.0 (2026-06-10)
15
+ - [Enhancement] Add `Node#register` to allow runtime key-value registration on compiled nodes without going through the static `setting` DSL. Useful for dynamic registries (e.g. named clusters) where setting names are not known at class-load time.
16
+ - [Enhancement] Replace version-gated `Warning[:performance]` with a `Warning.categories`-based loop that enables all opt-in Ruby warning categories automatically, picking up new categories (e.g. `strict_unused_block` in Ruby 3.4+) without future patches.
17
+
3
18
  ## 2.5.13 (2026-04-08)
4
19
  - [Enhancement] Extract `decorate_partitions` method from `StatisticsDecorator` to allow subclasses to filter which partitions are decorated (e.g. skip unassigned partitions in a consumer context).
5
20
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- karafka-core (2.5.13)
4
+ karafka-core (2.6.1)
5
5
  karafka-rdkafka (>= 0.20.0)
6
6
  logger (>= 1.6.0)
7
7
 
@@ -12,56 +12,62 @@ GEM
12
12
  reline (>= 0.6.0)
13
13
  docile (1.4.1)
14
14
  drb (2.2.3)
15
- ffi (1.17.2)
16
- ffi (1.17.2-aarch64-linux-gnu)
17
- ffi (1.17.2-aarch64-linux-musl)
18
- ffi (1.17.2-arm-linux-gnu)
19
- ffi (1.17.2-arm-linux-musl)
20
- ffi (1.17.2-arm64-darwin)
21
- ffi (1.17.2-x86-linux-gnu)
22
- ffi (1.17.2-x86-linux-musl)
23
- ffi (1.17.2-x86_64-darwin)
24
- ffi (1.17.2-x86_64-linux-gnu)
25
- ffi (1.17.2-x86_64-linux-musl)
15
+ ffi (1.17.4)
16
+ ffi (1.17.4-aarch64-linux-gnu)
17
+ ffi (1.17.4-aarch64-linux-musl)
18
+ ffi (1.17.4-arm-linux-gnu)
19
+ ffi (1.17.4-arm-linux-musl)
20
+ ffi (1.17.4-arm64-darwin)
21
+ ffi (1.17.4-x86-linux-gnu)
22
+ ffi (1.17.4-x86-linux-musl)
23
+ ffi (1.17.4-x86_64-darwin)
24
+ ffi (1.17.4-x86_64-linux-gnu)
25
+ ffi (1.17.4-x86_64-linux-musl)
26
26
  io-console (0.8.2)
27
- json (2.15.1)
28
- karafka-rdkafka (0.22.2)
29
- ffi (~> 1.15)
27
+ json (2.19.7)
28
+ karafka-rdkafka (0.27.2)
29
+ ffi (~> 1.17.1)
30
30
  json (> 2.0)
31
31
  logger
32
32
  mini_portile2 (~> 2.6)
33
33
  rake (> 12)
34
- karafka-rdkafka (0.22.2-aarch64-linux-gnu)
35
- ffi (~> 1.15)
34
+ karafka-rdkafka (0.27.2-aarch64-linux-gnu)
35
+ ffi (~> 1.17.1)
36
36
  json (> 2.0)
37
37
  logger
38
38
  mini_portile2 (~> 2.6)
39
39
  rake (> 12)
40
- karafka-rdkafka (0.22.2-arm64-darwin)
41
- ffi (~> 1.15)
40
+ karafka-rdkafka (0.27.2-aarch64-linux-musl)
41
+ ffi (~> 1.17.1)
42
42
  json (> 2.0)
43
43
  logger
44
44
  mini_portile2 (~> 2.6)
45
45
  rake (> 12)
46
- karafka-rdkafka (0.22.2-x86_64-linux-gnu)
47
- ffi (~> 1.15)
46
+ karafka-rdkafka (0.27.2-arm64-darwin)
47
+ ffi (~> 1.17.1)
48
48
  json (> 2.0)
49
49
  logger
50
50
  mini_portile2 (~> 2.6)
51
51
  rake (> 12)
52
- karafka-rdkafka (0.22.2-x86_64-linux-musl)
53
- ffi (~> 1.15)
52
+ karafka-rdkafka (0.27.2-x86_64-linux-gnu)
53
+ ffi (~> 1.17.1)
54
+ json (> 2.0)
55
+ logger
56
+ mini_portile2 (~> 2.6)
57
+ rake (> 12)
58
+ karafka-rdkafka (0.27.2-x86_64-linux-musl)
59
+ ffi (~> 1.17.1)
54
60
  json (> 2.0)
55
61
  logger
56
62
  mini_portile2 (~> 2.6)
57
63
  rake (> 12)
58
64
  logger (1.7.0)
59
65
  mini_portile2 (2.8.9)
60
- minitest (6.0.2)
66
+ minitest (6.0.6)
61
67
  drb (~> 2.0)
62
68
  prism (~> 1.5)
63
69
  prism (1.9.0)
64
- rake (13.3.0)
70
+ rake (13.4.2)
65
71
  reline (0.6.3)
66
72
  io-console (~> 0.5)
67
73
  simplecov (0.22.0)
@@ -70,7 +76,7 @@ GEM
70
76
  simplecov_json_formatter (~> 0.1)
71
77
  simplecov-html (0.13.2)
72
78
  simplecov_json_formatter (0.1.4)
73
- warning (1.5.0)
79
+ warning (1.6.0)
74
80
 
75
81
  PLATFORMS
76
82
  aarch64-linux-gnu
@@ -15,6 +15,49 @@ module Karafka
15
15
  # We need to be able to redefine children for deep copy
16
16
  attr_accessor :children
17
17
 
18
+ # Names that cannot be used as setting names because they would collide with the node
19
+ # internal state or the node public API: their accessors would shadow the node own
20
+ # readers (breaking for example `#deep_dup` or `#to_h`) and writers like `children=`
21
+ # would overwrite internal ivars. `#setting` and `#register` reject them upfront and
22
+ # `#ivar_backed?` keeps guarding the ivar mirror as defense in depth.
23
+ # Private method names are deliberately not reserved: that would make internal
24
+ # implementation details part of the public contract
25
+ RESERVED_NAMES = %i[
26
+ node_name
27
+ children
28
+ nestings
29
+ compiled
30
+ configs_refs
31
+ local_defs
32
+ setting
33
+ configure
34
+ to_h
35
+ deep_dup
36
+ register
37
+ compile
38
+ ].to_h { |name| [name, true] }.freeze
39
+
40
+ # Setting names that match this format can be backed by instance variables and use the
41
+ # fast `attr_reader` based readers. Others (e.g. registered names with dashes) fall back
42
+ # to the hash-based accessors
43
+ IVAR_NAMEABLE_FORMAT = /\A[A-Za-z_][A-Za-z0-9_]*\z/
44
+
45
+ private_constant :RESERVED_NAMES, :IVAR_NAMEABLE_FORMAT
46
+
47
+ class << self
48
+ # Builds each node through its own anonymous subclass. Since setting values are
49
+ # mirrored into instance variables for fast access and each node layout carries a
50
+ # different set of them, instantiating nodes directly from this class would grow its
51
+ # object shape variations past the Ruby limit, degrading ivar access for all nodes.
52
+ # A subclass per layout keeps shape variations per class minimal (late `setting`
53
+ # calls after inheritance or runtime `register` calls may add a few more, staying
54
+ # well under the limit). `#deep_dup` reuses the subclass of its template, so
55
+ # duplicated configs share shapes as well.
56
+ def new(...)
57
+ equal?(Node) ? Class.new(self).new(...) : super
58
+ end
59
+ end
60
+
18
61
  # @param node_name [Symbol] node name
19
62
  # @param nestings [Proc] block for nested settings
20
63
  # @param evaluate [Boolean] when false, skip evaluating the nestings block. Used by
@@ -31,12 +74,20 @@ module Karafka
31
74
 
32
75
  # Allows for a single leaf or nested node definition
33
76
  #
34
- # @param node_name [Symbol] setting or nested node name
77
+ # @param node_name [Symbol, String] setting or nested node name
35
78
  # @param default [Object] default value
36
79
  # @param constructor [#call, nil] callable or nil
37
80
  # @param lazy [Boolean] is this a lazy leaf
38
81
  # @param block [Proc] block for nested settings
82
+ # @raise [ArgumentError] when the name is reserved for the node internal state
39
83
  def setting(node_name, default: nil, constructor: nil, lazy: false, &block)
84
+ # Symbolize at definition time (same as `#register`) so the config store, accessors,
85
+ # `#to_h` and the compile state checks all agree on the key type also when a String
86
+ # name is provided
87
+ node_name = node_name.to_sym
88
+
89
+ prevent_reserved_names!(node_name)
90
+
40
91
  @children << if block
41
92
  Node.new(node_name, block)
42
93
  else
@@ -85,12 +136,12 @@ module Karafka
85
136
  # and non-side-effect usage on an instance/inherited.
86
137
  # @return [Node] duplicated node
87
138
  def deep_dup
88
- dupped = Node.new(node_name, nestings, evaluate: false)
139
+ # Same-layout nodes reuse the class of their template so they share object shapes
140
+ dupped = self.class.new(node_name, nestings, evaluate: false)
89
141
 
90
142
  children.each do |value|
91
143
  dupped.children << if value.is_a?(Leaf)
92
- # After inheritance we need to reload the state so the leafs are
93
- # recompiled again
144
+ # After inheritance we need to reload the state so the leafs are recompiled again
94
145
  value = value.dup
95
146
  value.compiled = false
96
147
  value
@@ -102,6 +153,36 @@ module Karafka
102
153
  dupped
103
154
  end
104
155
 
156
+ # Registers a key-value pair as a setting on an already-compiled node without going
157
+ # through the static `setting` DSL. Useful for dynamic registries (e.g. named clusters)
158
+ # where the keys are not known at class-load time.
159
+ #
160
+ # Unlike `setting`, which is designed to be called at class-definition time, `register`
161
+ # is safe to call at runtime because it:
162
+ # - appends a pre-compiled Leaf so `deep_dup` and `to_h` include it
163
+ # - sets `@configs_refs` directly so the reader accessor returns the value immediately
164
+ # - builds reader/writer accessors via the same `build_accessors` path
165
+ #
166
+ # Raises `ArgumentError` if the name is already registered to prevent silent overwrites.
167
+ #
168
+ # @param name [Symbol, String] setting name
169
+ # @param value [Object] the setting value assigned immediately; also used as the default
170
+ # when the node is deep-duped and recompiled on a new instance
171
+ # @raise [ArgumentError] when the name is already taken or reserved for the node
172
+ # internal state
173
+ def register(name, value)
174
+ name = name.to_sym
175
+
176
+ prevent_reserved_names!(name)
177
+
178
+ raise ArgumentError, "#{name} is already registered" if @configs_refs.key?(name)
179
+
180
+ leaf = Leaf.new(name, value, nil, true, false)
181
+ @children << leaf
182
+ build_accessors(leaf)
183
+ config_write(name, value)
184
+ end
185
+
105
186
  # Converts the settings definitions into end children
106
187
  # @note It runs once, after things are compiled, they will not be recompiled again
107
188
  def compile
@@ -135,7 +216,7 @@ module Karafka
135
216
  if lazy_leaf && !initialized
136
217
  build_dynamic_accessor(value)
137
218
  else
138
- @configs_refs[value.node_name] = initialized
219
+ config_write(value.node_name, initialized)
139
220
  end
140
221
  end
141
222
 
@@ -146,8 +227,7 @@ module Karafka
146
227
 
147
228
  # Defines a lazy evaluated read and writer that will re-evaluate in case value constructor
148
229
  # evaluates to `nil` or `false`. This allows us to define dynamic constructors that
149
- # can react to external conditions to become expected value once this value is
150
- # available
230
+ # can react to external conditions to become expected value once this value is available
151
231
  #
152
232
  # @param value [Leaf]
153
233
  def build_dynamic_accessor(value)
@@ -182,6 +262,12 @@ module Karafka
182
262
 
183
263
  # Builds regular accessors for value fetching
184
264
  #
265
+ # Settings with names that can form valid instance variables get `attr_reader` based
266
+ # readers backed by an ivar mirror of the config value. This is significantly faster
267
+ # than a method with a hash lookup, which matters since settings are read on hot paths
268
+ # across the whole ecosystem. `@configs_refs` remains the canonical store used by
269
+ # `#to_h`, `#compile` and `#register`, with `#config_write` keeping the mirror in sync.
270
+ #
185
271
  # @param value [Leaf]
186
272
  def build_accessors(value)
187
273
  reader_name = value.node_name.to_sym
@@ -194,17 +280,64 @@ module Karafka
194
280
  if reader_respond ? !@local_defs.key?(reader_name) : true
195
281
  @local_defs[reader_name] = true
196
282
 
197
- define_singleton_method(reader_name) do
198
- @configs_refs[reader_name]
283
+ if ivar_backed?(reader_name)
284
+ singleton_class.attr_reader(reader_name)
285
+ else
286
+ define_singleton_method(reader_name) do
287
+ @configs_refs[reader_name]
288
+ end
199
289
  end
200
290
  end
201
291
 
202
- return if respond_to?(:"#{value.node_name}=")
292
+ return if respond_to?(:"#{reader_name}=")
203
293
 
204
- define_singleton_method(:"#{value.node_name}=") do |new_value|
205
- @configs_refs[value.node_name] = new_value
294
+ if ivar_backed?(reader_name)
295
+ ivar_name = :"@#{reader_name}"
296
+
297
+ define_singleton_method(:"#{reader_name}=") do |new_value|
298
+ instance_variable_set(ivar_name, @configs_refs[reader_name] = new_value)
299
+ end
300
+ else
301
+ define_singleton_method(:"#{reader_name}=") do |new_value|
302
+ @configs_refs[reader_name] = new_value
303
+ end
206
304
  end
207
305
  end
306
+
307
+ # Writes a config value to the canonical store and mirrors it into the backing instance
308
+ # variable when the setting uses the fast ivar-backed reader
309
+ #
310
+ # @param name [Symbol, String] setting name
311
+ # @param value [Object] config value assigned to the setting
312
+ def config_write(name, value)
313
+ # Accessors operate on symbolized names, so the store has to be keyed consistently.
314
+ # This also guarantees that a String name matching a reserved internal name is
315
+ # recognized by the `ivar_backed?` guard and cannot corrupt the node internal state
316
+ name = name.to_sym
317
+
318
+ @configs_refs[name] = value
319
+ instance_variable_set(:"@#{name}", value) if ivar_backed?(name)
320
+ end
321
+
322
+ # @param name [Symbol] setting name
323
+ # @return [Boolean] true if this setting can be backed by an instance variable and use
324
+ # the fast `attr_reader` based reader
325
+ def ivar_backed?(name)
326
+ !RESERVED_NAMES.key?(name) && IVAR_NAMEABLE_FORMAT.match?(name)
327
+ end
328
+
329
+ # Rejects setting names that would collide with the node internal state. Without this,
330
+ # such names would shadow the node own accessors, breaking `#deep_dup` and silently
331
+ # corrupting internals on assignment (e.g. `config.children = value` hitting the node
332
+ # own `attr_writer`)
333
+ #
334
+ # @param name [Symbol] already symbolized setting name
335
+ # @raise [ArgumentError] when the name is reserved
336
+ def prevent_reserved_names!(name)
337
+ return unless RESERVED_NAMES.key?(name)
338
+
339
+ raise ArgumentError, "#{name} is a reserved name and cannot be used as a setting name"
340
+ end
208
341
  end
209
342
  end
210
343
  end
@@ -15,7 +15,7 @@ module Karafka
15
15
  DIG_MISS = Object.new
16
16
 
17
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
18
+ # `#call` / `#validate!` invocation. Safe because scope is never mutated - it is only
19
19
  # used in `scope + rule.path` which creates a new Array.
20
20
  EMPTY_ARRAY = [].freeze
21
21
 
@@ -81,6 +81,12 @@ module Karafka
81
81
 
82
82
  # Runs the validation
83
83
  #
84
+ # The per-rule handling is inlined instead of dispatching to per-type methods because
85
+ # this runs per rule per validation, including the per-message validations in
86
+ # WaterDrop. Required and optional rules share the whole flow except the missing-key
87
+ # handling. `DIG_MISS` is compared via `#equal?` so we never dispatch `#==` to the
88
+ # validated (user-provided) values.
89
+ #
84
90
  # @param data [Hash] hash with data we want to validate
85
91
  # @param scope [Array<String>] scope of this contract (if any) or empty array if no parent
86
92
  # scope is needed if contract starts from root
@@ -89,13 +95,28 @@ module Karafka
89
95
  errors = []
90
96
 
91
97
  self.class.rules.each do |rule|
92
- case rule.type
93
- when :required
94
- validate_required(data, rule, errors, scope)
95
- when :optional
96
- validate_optional(data, rule, errors, scope)
97
- when :virtual
98
- validate_virtual(data, rule, errors, scope)
98
+ if rule.type == :virtual
99
+ result = rule.validator.call(data, errors, self)
100
+
101
+ next if result == true
102
+
103
+ result&.each do |sub_result|
104
+ sub_result[0] = scope + sub_result[0]
105
+ end
106
+
107
+ errors.push(*result)
108
+ else
109
+ for_checking = dig(data, rule.path)
110
+
111
+ if DIG_MISS.equal?(for_checking)
112
+ errors << [scope + rule.path, :missing] if rule.type == :required
113
+ else
114
+ result = rule.validator.call(for_checking, data, errors, self)
115
+
116
+ next if result == true
117
+
118
+ errors << [scope + rule.path, result || :format]
119
+ end
99
120
  end
100
121
  end
101
122
 
@@ -121,99 +142,39 @@ module Karafka
121
142
 
122
143
  private
123
144
 
124
- # Runs validation for rules on fields that are required and adds errors (if any) to the
125
- # errors array
126
- #
127
- # @param data [Hash] input hash
128
- # @param rule [Rule] validation rule
129
- # @param errors [Array] array with errors from previous rules (if any)
130
- # @param scope [Array<String>]
131
- def validate_required(data, rule, errors, scope)
132
- for_checking = dig(data, rule.path)
133
-
134
- # We need to compare `DIG_MISS` against stuff because of the ownership of the `#==`
135
- # method
136
- if for_checking == DIG_MISS
137
- errors << [scope + rule.path, :missing]
138
- else
139
- result = rule.validator.call(for_checking, data, errors, self)
140
-
141
- return if result == true
142
-
143
- errors << [scope + rule.path, result || :format]
144
- end
145
- end
146
-
147
- # Runs validation for rules on fields that are optional and adds errors (if any) to the
148
- # errors array
149
- #
150
- # @param data [Hash] input hash
151
- # @param rule [Rule] validation rule
152
- # @param errors [Array] array with errors from previous rules (if any)
153
- # @param scope [Array<String>]
154
- def validate_optional(data, rule, errors, scope)
155
- for_checking = dig(data, rule.path)
156
-
157
- return if for_checking == DIG_MISS
158
-
159
- result = rule.validator.call(for_checking, data, errors, self)
160
-
161
- return if result == true
162
-
163
- errors << [scope + rule.path, result || :format]
164
- end
165
-
166
- # Runs validation for rules on virtual fields (aggregates, etc) and adds errors (if any) to
167
- # the errors array
168
- #
169
- # @param data [Hash] input hash
170
- # @param rule [Rule] validation rule
171
- # @param errors [Array] array with errors from previous rules (if any)
172
- # @param scope [Array<String>]
173
- def validate_virtual(data, rule, errors, scope)
174
- result = rule.validator.call(data, errors, self)
175
-
176
- return if result == true
177
-
178
- result&.each do |sub_result|
179
- sub_result[0] = scope + sub_result[0]
180
- end
181
-
182
- errors.push(*result)
183
- end
184
-
185
145
  # Tries to dig for a given key in a hash and returns it with indication whether or not it
186
146
  # was possible to find it (dig returns nil and we don't know if it wasn't the digged key
187
147
  # value)
188
148
  #
149
+ # Uses `Hash#fetch` with the `DIG_MISS` sentinel as the default, which resolves presence
150
+ # and value in a single hash lookup instead of a `key?` check followed by `[]`. This
151
+ # runs per rule per validation, including the per-message validations in WaterDrop,
152
+ # hence the lookup count matters. `fetch` with a default ignores `default_proc`, same
153
+ # as the previous `key?` based logic.
154
+ #
189
155
  # @param data [Hash]
190
156
  # @param keys [Array<Symbol>]
191
157
  # @return [DIG_MISS, Object] found element or DIGG_MISS indicating that not found
192
158
  def dig(data, keys)
193
159
  case keys.length
194
160
  when 1
195
- key = keys[0]
196
-
197
- return DIG_MISS unless data.key?(key)
198
-
199
- data[key]
161
+ data.fetch(keys[0], DIG_MISS)
200
162
  when 2
201
- key1 = keys[0]
202
-
203
- return DIG_MISS unless data.key?(key1)
163
+ mid = data.fetch(keys[0], DIG_MISS)
204
164
 
205
- mid = data[key1]
165
+ return DIG_MISS if DIG_MISS.equal?(mid)
166
+ return DIG_MISS unless mid.is_a?(Hash)
206
167
 
207
- return DIG_MISS unless mid.is_a?(Hash) && mid.key?(keys[1])
208
-
209
- mid[keys[1]]
168
+ mid.fetch(keys[1], DIG_MISS)
210
169
  else
211
170
  current = data
212
171
 
213
172
  keys.each do |nesting|
214
- return DIG_MISS unless current.key?(nesting)
173
+ return DIG_MISS unless current.is_a?(Hash)
174
+
175
+ current = current.fetch(nesting, DIG_MISS)
215
176
 
216
- current = current[nesting]
177
+ return DIG_MISS if DIG_MISS.equal?(current)
217
178
  end
218
179
 
219
180
  current
@@ -28,7 +28,15 @@ module Karafka
28
28
  # @param event_id [String, Symbol] event id
29
29
  # @param payload [Hash]
30
30
  def instrument(event_id, payload = EMPTY_HASH, &)
31
- full_event_name = @mapped_events[event_id] ||= [event_id, @namespace].compact.join(".")
31
+ # With no namespace, string event ids already are the full event names. This is the
32
+ # case for all the events in the Karafka ecosystem, so we can skip the mapping hash
33
+ # lookup on this hot path. Symbols still go through the mapping to be converted into
34
+ # strings without allocating on each call.
35
+ full_event_name = if @namespace.nil? && event_id.is_a?(String)
36
+ event_id
37
+ else
38
+ @mapped_events[event_id] ||= [event_id, @namespace].compact.join(".")
39
+ end
32
40
 
33
41
  @notifications_bus.instrument(full_event_name, payload, &)
34
42
  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.13"
7
+ VERSION = "2.6.1"
8
8
  end
9
9
  end
data/package-lock.json CHANGED
@@ -217,9 +217,9 @@
217
217
  }
218
218
  },
219
219
  "node_modules/picomatch": {
220
- "version": "2.3.1",
221
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
222
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
220
+ "version": "2.3.2",
221
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
222
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
223
223
  "dev": true,
224
224
  "license": "MIT",
225
225
  "engines": {
@@ -286,9 +286,9 @@
286
286
  }
287
287
  },
288
288
  "node_modules/smol-toml": {
289
- "version": "1.6.0",
290
- "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
291
- "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==",
289
+ "version": "1.6.1",
290
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
291
+ "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
292
292
  "dev": true,
293
293
  "license": "BSD-3-Clause",
294
294
  "engines": {
@@ -312,9 +312,9 @@
312
312
  }
313
313
  },
314
314
  "node_modules/yaml": {
315
- "version": "2.8.2",
316
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
317
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
315
+ "version": "2.9.0",
316
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
317
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
318
318
  "dev": true,
319
319
  "license": "ISC",
320
320
  "bin": {
data/renovate.json CHANGED
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "minimumReleaseAge": "7 days",
17
17
  "matchDepNames": [
18
- "/*/"
18
+ "*"
19
19
  ]
20
20
  },
21
21
  {
@@ -30,11 +30,25 @@
30
30
  "ruby/setup-ruby",
31
31
  "ruby"
32
32
  ],
33
- "groupName": "ruby setup"
33
+ "groupName": "ruby setup",
34
+ "internalChecksFilter": "strict"
35
+ },
36
+ {
37
+ "description": "Let setup-ruby pass age gate before ruby so it is ready when the group PR is created",
38
+ "matchPackageNames": [
39
+ "ruby/setup-ruby"
40
+ ],
41
+ "minimumReleaseAge": "5 days"
34
42
  }
35
43
  ],
36
44
  "labels": [
37
45
  "dependencies"
38
46
  ],
39
- "minimumReleaseAge": "7 days"
47
+ "minimumReleaseAge": "7 days",
48
+ "lockFileMaintenance": {
49
+ "enabled": true,
50
+ "schedule": [
51
+ "before 4am on the first day of the month"
52
+ ]
53
+ }
40
54
  }
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.13
4
+ version: 2.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -120,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
120
  - !ruby/object:Gem::Version
121
121
  version: '0'
122
122
  requirements: []
123
- rubygems_version: 4.0.6
123
+ rubygems_version: 4.0.10
124
124
  specification_version: 4
125
125
  summary: Karafka ecosystem core modules
126
126
  test_files: []