karafka-core 2.6.5 → 2.6.6

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: 236e0814cbc0a24dc6758f75c71057e7744c0827f1c9cb4b5be5f305bceefc46
4
- data.tar.gz: b3d9b8d7b42e9af2aaa2609bd929dab513375aa86ebf49d198c94e11cb81427c
3
+ metadata.gz: 69c1a45639ef9a5f60dfe01427637a68be3a284a1d3cc6645f35c7f1bc4db60a
4
+ data.tar.gz: af342b72f3862e8264fbc09e4e6ba473ff13a0b2615ea78738071f5b366e9c48
5
5
  SHA512:
6
- metadata.gz: e6cabe34dc4bb8449ba87cec2d1dc7d120277fecc771ee7a90509f34454e28705a7ab3e883a68799ac128badd9e5f8ad3d5d6898e2e4d1565568dad3b98f0bb9
7
- data.tar.gz: 80072d69c0ae0eb59b933c83d530e3099bf16f16c596b34bdf16c70f0986938c599dbb65c13331c69e7881119cde8c200fc51713729e84a085c15dd6d989fdba
6
+ metadata.gz: 0ed70ee79bf6cb77ec89df3e68d30829b70c79dadc699b491006c3ac536009230fdd188996a7386c1cce0140625c04002a00575b7c5fda6b7d82a172b241e6e4
7
+ data.tar.gz: c7d33f57867534c8afa1ab28a00f63b4d806fc2286c61bdb31a0111b8a81048e8a1ce654b8ff5dbc0dd7736d70ad8db92738b264c7ed6d3124ba0fc6da96105e
@@ -36,7 +36,7 @@ jobs:
36
36
  run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
37
37
 
38
38
  - name: Set up Ruby
39
- uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0
39
+ uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
40
40
  with:
41
41
  ruby-version: ${{matrix.ruby}}
42
42
  bundler: 'latest'
@@ -68,7 +68,7 @@ jobs:
68
68
  with:
69
69
  fetch-depth: 0
70
70
  - name: Set up Ruby
71
- uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0
71
+ uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
72
72
  with:
73
73
  ruby-version: '4.0.7'
74
74
  bundler-cache: true
@@ -85,7 +85,7 @@ jobs:
85
85
  with:
86
86
  fetch-depth: 0
87
87
  - name: Set up Ruby
88
- uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0
88
+ uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
89
89
  with:
90
90
  ruby-version: '4.0.7'
91
91
  bundler-cache: true
@@ -24,7 +24,7 @@ jobs:
24
24
  fetch-depth: 0
25
25
 
26
26
  - name: Set up Ruby
27
- uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0
27
+ uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
28
28
  with:
29
29
  bundler-cache: false
30
30
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Karafka Core Changelog
2
2
 
3
+ ## 2.6.6 (2026-10-25)
4
+ - [Fix] Make `Taggable::Tags` safe to read while another thread adds or removes tags. On JRuby a concurrent read could raise `NullPointerException`.
5
+ - [Fix] Make `Taggable#tags` return one `Tags` per object when several threads call it first. On JRuby each thread could get its own `Tags`, and tags were lost.
6
+ - [Fix] Make `Notifications#register_event` safe while other threads call `#instrument`. On JRuby a concurrent registration could make a registered event look unregistered or dispatch it without its method.
7
+ - [Fix] Make the `Monitor` event name cache thread-safe for a namespace or Symbol event ids, so on JRuby concurrent `#instrument` calls no longer corrupt it.
8
+
3
9
  ## 2.6.5 (2026-10-19)
4
10
  - **[EOL]** Drop Ruby 3.2 support due to EOL; require Ruby 3.3+.
5
11
  - [Change] Use the offending value (event id, setting name, class or constant name) as the message of errors raised by `Notifications`, `Configurable::Node`, `Configurable::Importer` and the spec locators.
@@ -42,7 +48,7 @@
42
48
  - [Change] Assigning a setting on a frozen config node now raises `FrozenError` (previously the write silently mutated internal storage despite the freeze).
43
49
 
44
50
  ## 2.6.0 (2026-06-10)
45
- - [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.
51
+ - [Enhancement] Add `Node#register` to register keys at runtime on compiled nodes, for dynamic registries (e.g. named clusters) whose setting names are not known at class-load time.
46
52
  - [Enhancement] Enable all opt-in Ruby warning categories via `Warning.categories`, so new ones (e.g. `strict_unused_block` in Ruby 3.4+) are picked up automatically.
47
53
 
48
54
  ## 2.5.13 (2026-04-08)
@@ -54,24 +60,24 @@
54
60
  ## 2.5.11 (2026-04-02)
55
61
  - [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.
56
62
  - [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.
57
- - [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.
58
- - [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.
63
+ - [Enhancement] Stop allocating an Array on every `Contract#call` and `#validate!` call (~1.36x faster dispatch).
64
+ - [Enhancement] Speed up `Notifications#instrument` with multiple listeners (~1.12x faster event dispatch).
59
65
  - [Enhancement] Cache a frozen success `Result` singleton via `Result.success` to eliminate 1 object allocation per successful `Contract#call` on the happy path.
60
- - [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.
61
- - [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.
66
+ - [Enhancement] Speed up `Node#deep_dup` ~14x and cut its allocations from ~620 to ~66 objects for large configs.
67
+ - [Enhancement] Stop allocating an Array on every `CallbacksManager#call` call.
62
68
  - [Enhancement] Build the `Event` payload hash lazily on `#payload` access, saving one Hash allocation per `Notifications#instrument` call with `#[]` access, for ~1.7x faster event dispatch.
63
- - [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).
69
+ - [Enhancement] Speed up `StatisticsDecorator#diff` by ~13% at scale (10 brokers, 20 topics, 2000 partitions).
64
70
  - [Enhancement] Reorder `StatisticsDecorator#diff` type checks to test `Numeric` before `Hash`, matching the ~80% numeric value distribution in librdkafka statistics.
65
71
  - [Enhancement] Support an `only_keys` option in `StatisticsDecorator` to decorate only the listed numeric keys. With `excluded_keys`, this cuts decoration cost from ~80ms to ~8.5ms per call on large clusters.
66
- - [Enhancement] Cache `Tags#to_a` values array and invalidate on `add`/`delete`/`clear` to avoid allocating a new Array and running `uniq` on every call, yielding ~7x faster reads at 5 tags and ~28x faster at 20 tags.
72
+ - [Enhancement] Cache `Tags#to_a` (~7x faster reads at 5 tags, ~28x at 20 tags).
67
73
 
68
74
  ## 2.5.10 (2026-03-02)
69
75
  - [Enhancement] Introduce `MinitestLocator` helper for minitest/spec subject class auto-discovery from test file paths.
70
76
 
71
77
  ## 2.5.9 (2026-03-02)
72
- - [Enhancement] Optimize `StatisticsDecorator` to eliminate per-hash Array allocations by using `each_pair` with a per-call pending-writes buffer instead of `current.keys.each`, reducing allocations from tens of thousands to one per call at scale.
78
+ - [Enhancement] Reduce `StatisticsDecorator` allocations from tens of thousands to one per call at scale.
73
79
  - [Enhancement] Inline `StatisticsDecorator#append` and `#suffix_keys_for` into `#diff` to reduce method call overhead by ~96% (from ~915k to ~39k calls at 6400 partitions).
74
- - [Enhancement] Support `excluded_keys` option in `StatisticsDecorator` to skip decoration of unused librdkafka statistics subtrees (e.g. broker toppars, window stats), yielding ~28% additional speedup at scale.
80
+ - [Enhancement] Support an `excluded_keys` option in `StatisticsDecorator` to skip unused librdkafka statistics subtrees (e.g. broker toppars, window stats), ~28% faster at scale.
75
81
 
76
82
  ## 2.5.8 (2025-11-23)
77
83
  - [Enhancement] Memoize `StatisticsDecorator` suffix keys to reduce string allocations (#268).
data/Gemfile CHANGED
@@ -5,7 +5,6 @@ source "https://rubygems.org"
5
5
  gemspec
6
6
 
7
7
  group :test do
8
- gem "byebug"
9
8
  gem "minitest"
10
9
  gem "simplecov"
11
10
  gem "warning"
data/Gemfile.lock CHANGED
@@ -1,15 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- karafka-core (2.6.5)
4
+ karafka-core (2.6.6)
5
5
  karafka-rdkafka (>= 0.20.0)
6
6
  logger (>= 1.6.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- byebug (13.0.0)
12
- reline (>= 0.6.0)
13
11
  drb (2.2.3)
14
12
  ffi (1.17.4)
15
13
  ffi (1.17.4-aarch64-linux-gnu)
@@ -22,7 +20,6 @@ GEM
22
20
  ffi (1.17.4-x86_64-darwin)
23
21
  ffi (1.17.4-x86_64-linux-gnu)
24
22
  ffi (1.17.4-x86_64-linux-musl)
25
- io-console (0.9.4)
26
23
  json (3.0.2)
27
24
  karafka-rdkafka (0.30.2)
28
25
  ffi (~> 1.17.1)
@@ -67,8 +64,6 @@ GEM
67
64
  prism (~> 1.5)
68
65
  prism (1.9.0)
69
66
  rake (13.4.2)
70
- reline (0.7.0)
71
- io-console (~> 0.5)
72
67
  simplecov (1.3.2)
73
68
  warning (1.6.0)
74
69
 
@@ -86,7 +81,6 @@ PLATFORMS
86
81
  x86_64-linux-musl
87
82
 
88
83
  DEPENDENCIES
89
- byebug
90
84
  karafka-core!
91
85
  minitest
92
86
  simplecov
data/README.md CHANGED
@@ -17,6 +17,6 @@ It includes:
17
17
 
18
18
  First, thank you for considering contributing to the Karafka ecosystem! It's people like you that make the open source community such a great community!
19
19
 
20
- Each pull request must pass all the RSpec specs, integration tests and meet our quality requirements.
20
+ Each pull request must pass all the tests and meet our quality requirements.
21
21
 
22
22
  Fork it, update and wait for the Github Actions results.
@@ -21,6 +21,7 @@ module Karafka
21
21
  @notifications_bus = notifications_bus
22
22
  @namespace = namespace
23
23
  @mapped_events = {}
24
+ @mutex = Mutex.new
24
25
  end
25
26
 
26
27
  # Passes the instrumentation block (if any) into the notifications bus
@@ -35,7 +36,7 @@ module Karafka
35
36
  full_event_name = if @namespace.nil? && event_id.is_a?(String)
36
37
  event_id
37
38
  else
38
- @mapped_events[event_id] ||= [event_id, @namespace].compact.join(".")
39
+ @mapped_events[event_id] || map_event(event_id)
39
40
  end
40
41
 
41
42
  @notifications_bus.instrument(full_event_name, payload, &)
@@ -65,6 +66,24 @@ module Karafka
65
66
  def listeners
66
67
  @notifications_bus.listeners
67
68
  end
69
+
70
+ private
71
+
72
+ # Builds and caches the full event name for an event id
73
+ #
74
+ # @param event_id [String, Symbol] event id
75
+ # @return [String] full event name
76
+ def map_event(event_id)
77
+ @mutex.synchronize do
78
+ # Copy-on-write: #instrument reads the Hash without the lock and on JRuby a Hash that
79
+ # is written by many threads at once can get corrupted, so we replace it instead.
80
+ @mapped_events[event_id] || begin
81
+ full_event_name = [event_id, @namespace].compact.join(".")
82
+ @mapped_events = @mapped_events.merge(event_id => full_event_name).freeze
83
+ full_event_name
84
+ end
85
+ end
86
+ end
68
87
  end
69
88
  end
70
89
  end
@@ -38,8 +38,13 @@ module Karafka
38
38
  # @param event_id [String] event id
39
39
  def register_event(event_id)
40
40
  @mutex.synchronize do
41
- @listeners[event_id] = []
42
- @events_methods_map[event_id] = :"on_#{event_id.to_s.tr(".", "_")}"
41
+ # Copy-on-write: #instrument reads both Hashes without the lock, and on JRuby a Hash
42
+ # that grows while another thread reads it can return nil for a registered key. The
43
+ # methods map goes first, so an event visible in @listeners always has its method.
44
+ @events_methods_map = @events_methods_map.merge(
45
+ event_id => :"on_#{event_id.to_s.tr(".", "_")}"
46
+ )
47
+ @listeners = @listeners.merge(event_id => [])
43
48
  end
44
49
  end
45
50
 
@@ -48,15 +53,16 @@ module Karafka
48
53
  # @param event_id [String] the key of the event to clear listeners for.
49
54
  def clear(event_id = nil)
50
55
  @mutex.synchronize do
51
- # Copy-on-write: replace the per-event arrays rather than mutating them in place, so a
52
- # dispatch iterating a previously captured array is unaffected (see #notify_listeners).
56
+ # Copy-on-write: replace the Hash and the per-event arrays rather than mutating them in
57
+ # place, so lock-free reads in #instrument and a dispatch iterating a previously
58
+ # captured array are unaffected (see #register_event and #notify_listeners).
53
59
  unless event_id
54
- @listeners.transform_values! { [] }
60
+ @listeners = @listeners.transform_values { [] }
55
61
  return
56
62
  end
57
63
 
58
64
  if @listeners.key?(event_id)
59
- @listeners[event_id] = []
65
+ @listeners = @listeners.merge(event_id => [])
60
66
  return
61
67
  end
62
68
 
@@ -85,17 +91,21 @@ module Karafka
85
91
 
86
92
  raise EventNotRegistered, event_id unless @listeners.key?(event_id)
87
93
 
88
- # Copy-on-write: append by replacing the array, never mutating the one a concurrent
89
- # dispatch may be iterating (see #notify_listeners).
90
- @listeners[event_id] += [block]
94
+ # Copy-on-write: append by replacing the Hash and the array, never mutating the ones
95
+ # a concurrent #instrument or dispatch may be reading (see #notify_listeners).
96
+ @listeners = @listeners.merge(event_id => @listeners[event_id] + [block])
91
97
  else
92
98
  listener = event_id_or_listener
93
99
 
94
- @listeners.each_key do |reg_event_id|
100
+ listeners = @listeners.dup
101
+
102
+ @listeners.each do |reg_event_id, event_listeners|
95
103
  next unless listener.respond_to?(@events_methods_map[reg_event_id])
96
104
 
97
- @listeners[reg_event_id] += [listener]
105
+ listeners[reg_event_id] = event_listeners + [listener]
98
106
  end
107
+
108
+ @listeners = listeners
99
109
  end
100
110
  end
101
111
  end
@@ -109,9 +119,10 @@ module Karafka
109
119
  # unsubscribe(my_listener)
110
120
  def unsubscribe(listener_or_block)
111
121
  @mutex.synchronize do
112
- # Copy-on-write: rebuild each array without the listener instead of deleting in place,
113
- # so a dispatch iterating a previously captured array still sees the full set.
114
- @listeners.transform_values! { |event_listeners| event_listeners - [listener_or_block] }
122
+ # Copy-on-write: rebuild the Hash and each array without the listener instead of
123
+ # deleting in place, so a dispatch iterating a previously captured array still sees
124
+ # the full set.
125
+ @listeners = @listeners.transform_values { |event_listeners| event_listeners - [listener_or_block] }
115
126
  end
116
127
  end
117
128
 
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Core
5
+ module Taggable
6
+ class Tags
7
+ # Tags by name and their unique values, published together so a reader never sees one
8
+ # without the other
9
+ Snapshot = Struct.new(:tags, :values)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -9,34 +9,33 @@ module Karafka
9
9
  class Tags
10
10
  # Creates new tags accumulator
11
11
  def initialize
12
- @accu = {}
13
- @values_cache = nil
12
+ @mutex = Mutex.new
13
+ @snapshot = Snapshot.new({}.freeze, []).freeze
14
14
  end
15
15
 
16
16
  # Adds a tag with a given name to tags
17
17
  # @param name [Symbol] name we want to use for a given tag
18
18
  # @param tag [#to_s] any object that can be converted into a string via `#to_s`
19
19
  def add(name, tag)
20
- @accu[name] = tag.to_s
21
- @values_cache = nil
20
+ value = tag.to_s
21
+
22
+ @mutex.synchronize { publish(@snapshot.tags.merge(name => value)) }
22
23
  end
23
24
 
24
25
  # Removes all the tags
25
26
  def clear
26
- @accu.clear
27
- @values_cache = nil
27
+ @mutex.synchronize { publish({}) }
28
28
  end
29
29
 
30
30
  # Removes a tag with a given name
31
31
  # @param name [Symbol] name of the tag
32
32
  def delete(name)
33
- @accu.delete(name)
34
- @values_cache = nil
33
+ @mutex.synchronize { publish(@snapshot.tags.except(name)) }
35
34
  end
36
35
 
37
36
  # @return [Array<String>] all unique tags registered
38
37
  def to_a
39
- @values_cache ||= @accu.values.uniq
38
+ @snapshot.values
40
39
  end
41
40
 
42
41
  # @param _args [Object] anything that the standard `to_json` accepts
@@ -50,6 +49,17 @@ module Karafka
50
49
  def as_json(*_args)
51
50
  to_a
52
51
  end
52
+
53
+ private
54
+
55
+ # Replaces the snapshot with one built from the given tags. Readers do not lock, so the tags
56
+ # are never changed in place: each write publishes a new snapshot in one assignment.
57
+ #
58
+ # @param tags [Hash{Symbol => String}] all the tags after the change
59
+ # @return [Snapshot] the published snapshot
60
+ def publish(tags)
61
+ @snapshot = Snapshot.new(tags.freeze, tags.values.uniq).freeze
62
+ end
53
63
  end
54
64
  end
55
65
  end
@@ -8,9 +8,31 @@ module Karafka
8
8
  #
9
9
  # Tags will be converted to strings when they are added
10
10
  module Taggable
11
+ # One lock for the first `#tags` call of every object. That call is rare, and a lock per
12
+ # object would need a safe way to create it as well. Kept off the constants, because
13
+ # constants of this module are visible in every class that includes it.
14
+ @tags_lock = Mutex.new
15
+
16
+ class << self
17
+ # @return [Mutex] lock guarding the lazy creation of tags
18
+ attr_reader :tags_lock
19
+ end
20
+
11
21
  # @return [::Karafka::Core::Taggable::Tags] tags object
12
22
  def tags
13
- @tags ||= Taggable::Tags.new
23
+ @tags || taggable_build_tags
24
+ end
25
+
26
+ private
27
+
28
+ # Do not inline this into `#tags`. A `#synchronize` block there makes every warm `#tags` call
29
+ # about 2.5x slower on JRuby, even when `@tags` is already set. A hand-written `lock` /
30
+ # `ensure unlock` keeps it fast but can leave the lock held when an async exception
31
+ # (`Thread#raise`, `Timeout`) lands between `lock` and `begin`.
32
+ #
33
+ # @return [::Karafka::Core::Taggable::Tags] tags object
34
+ def taggable_build_tags
35
+ Taggable.tags_lock.synchronize { @tags ||= Taggable::Tags.new }
14
36
  end
15
37
  end
16
38
  end
@@ -3,6 +3,6 @@
3
3
  module Karafka
4
4
  module Core
5
5
  # Current Karafka::Core version. We follow the versioning schema of given Karafka version
6
- VERSION = "2.6.5"
6
+ VERSION = "2.6.6"
7
7
  end
8
8
  end
data/lib/karafka-core.rb CHANGED
@@ -24,6 +24,7 @@ require "karafka/core/instrumentation"
24
24
  require "karafka/core/instrumentation/callbacks_manager"
25
25
  require "karafka/core/taggable"
26
26
  require "karafka/core/taggable/tags"
27
+ require "karafka/core/taggable/tags/snapshot"
27
28
  require "karafka/core/patches/rdkafka/bindings"
28
29
 
29
30
  # Karafka framework main namespace
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.6.5
4
+ version: 2.6.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -93,6 +93,7 @@ files:
93
93
  - lib/karafka/core/patches/rdkafka/bindings.rb
94
94
  - lib/karafka/core/taggable.rb
95
95
  - lib/karafka/core/taggable/tags.rb
96
+ - lib/karafka/core/taggable/tags/snapshot.rb
96
97
  - lib/karafka/core/version.rb
97
98
  - log/.gitkeep
98
99
  - package-lock.json