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 +4 -4
- data/.github/workflows/ci.yml +10 -10
- data/.github/workflows/push.yml +2 -2
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +32 -26
- data/lib/karafka/core/configurable/node.rb +145 -12
- data/lib/karafka/core/contractable/contract.rb +44 -83
- data/lib/karafka/core/monitoring/monitor.rb +9 -1
- data/lib/karafka/core/version.rb +1 -1
- data/package-lock.json +9 -9
- data/renovate.json +17 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d9f6ba569f0c3a911cbe3ce80c545f3069b5aa2560782cf88bd11c97baa0432
|
|
4
|
+
data.tar.gz: 54f274da23e99841809f884ea871ce9a55705d76d3c4e5c40b0ef2ffbe0ae75a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a6c6ba64216479cebb2c4f574dff194ae6665189f382b0a60193c5b4ec3ab4d55f4ff97d7fb3fc90800f105a5284da27493b5694adff6e1a6c0738fa5795f65
|
|
7
|
+
data.tar.gz: 8ea0eddcd9ae95603041f8a74a1a26311cd9aa13744759dd6e7a699eb6ce682fd89596ef85e77ee2fd280d4aa3eca14d8f2ddd7a75dc322fe6e7bb33903ac7b6
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
- ruby: '4.0'
|
|
30
30
|
coverage: 'true'
|
|
31
31
|
steps:
|
|
32
|
-
- uses: actions/checkout@
|
|
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@
|
|
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@
|
|
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@
|
|
72
|
+
uses: ruby/setup-ruby@12fd324f1d0b43274fdc8130f6980590a667c455 # v1.312.0
|
|
73
73
|
with:
|
|
74
|
-
ruby-version: '4.0.
|
|
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@
|
|
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@
|
|
89
|
+
uses: ruby/setup-ruby@12fd324f1d0b43274fdc8130f6980590a667c455 # v1.312.0
|
|
90
90
|
with:
|
|
91
|
-
ruby-version: '4.0.
|
|
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@
|
|
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@
|
|
104
|
+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
105
105
|
with:
|
|
106
106
|
node-version: '20'
|
|
107
107
|
cache: 'npm'
|
data/.github/workflows/push.yml
CHANGED
|
@@ -19,12 +19,12 @@ jobs:
|
|
|
19
19
|
id-token: write
|
|
20
20
|
|
|
21
21
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
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@
|
|
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@
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
-
ffi (1.17.
|
|
17
|
-
ffi (1.17.
|
|
18
|
-
ffi (1.17.
|
|
19
|
-
ffi (1.17.
|
|
20
|
-
ffi (1.17.
|
|
21
|
-
ffi (1.17.
|
|
22
|
-
ffi (1.17.
|
|
23
|
-
ffi (1.17.
|
|
24
|
-
ffi (1.17.
|
|
25
|
-
ffi (1.17.
|
|
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.
|
|
28
|
-
karafka-rdkafka (0.
|
|
29
|
-
ffi (~> 1.
|
|
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.
|
|
35
|
-
ffi (~> 1.
|
|
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.
|
|
41
|
-
ffi (~> 1.
|
|
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.
|
|
47
|
-
ffi (~> 1.
|
|
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.
|
|
53
|
-
ffi (~> 1.
|
|
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.
|
|
66
|
+
minitest (6.0.6)
|
|
61
67
|
drb (~> 2.0)
|
|
62
68
|
prism (~> 1.5)
|
|
63
69
|
prism (1.9.0)
|
|
64
|
-
rake (13.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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?(:"#{
|
|
292
|
+
return if respond_to?(:"#{reader_name}=")
|
|
203
293
|
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
return DIG_MISS unless data.key?(key1)
|
|
163
|
+
mid = data.fetch(keys[0], DIG_MISS)
|
|
204
164
|
|
|
205
|
-
|
|
165
|
+
return DIG_MISS if DIG_MISS.equal?(mid)
|
|
166
|
+
return DIG_MISS unless mid.is_a?(Hash)
|
|
206
167
|
|
|
207
|
-
|
|
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.
|
|
173
|
+
return DIG_MISS unless current.is_a?(Hash)
|
|
174
|
+
|
|
175
|
+
current = current.fetch(nesting, DIG_MISS)
|
|
215
176
|
|
|
216
|
-
|
|
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
|
-
|
|
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
|
data/lib/karafka/core/version.rb
CHANGED
data/package-lock.json
CHANGED
|
@@ -217,9 +217,9 @@
|
|
|
217
217
|
}
|
|
218
218
|
},
|
|
219
219
|
"node_modules/picomatch": {
|
|
220
|
-
"version": "2.3.
|
|
221
|
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.
|
|
222
|
-
"integrity": "sha512-
|
|
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.
|
|
290
|
-
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.
|
|
291
|
-
"integrity": "sha512-
|
|
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.
|
|
316
|
-
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.
|
|
317
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
123
|
+
rubygems_version: 4.0.10
|
|
124
124
|
specification_version: 4
|
|
125
125
|
summary: Karafka ecosystem core modules
|
|
126
126
|
test_files: []
|