karafka 2.4.3 → 2.4.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c37b855609a9d20ca0b5cf13875d30ccb71b4072b3f07886f10445ce761c6e1e
4
- data.tar.gz: 852fa3340f0dd1cbc2286823a2451c635568440c3e753e7235a72dc85289fcf5
3
+ metadata.gz: fe76e139b2007e11b0cb9393bc94e2b5b9cad5dc40d69b5ad2bfa34a0555e2b8
4
+ data.tar.gz: e288c63648d17388f577a45b51267e30fceb4726def0a42c40a9133de27d93f8
5
5
  SHA512:
6
- metadata.gz: 373b460007cefdb039b5f2fd00abdc722f42c5bd8f324dad121c7e293264ca4a63cb63cb4fa5057e0f2ff856fe6c28073c04d2ffb21250dd534408ee511807c9
7
- data.tar.gz: 595e408578801d949bb2b1e8d3337e8535473b80c7a1db34e56822214de9fd2083a594433e264ed8675c2292cd3aa73bacaef44938ad7a4cd743292097fd4d8f
6
+ metadata.gz: b5e46439a216e66c728ceb74070832407d39b1ebe06d21c858f3e3a0e8e599df15b749318131aa82a5bc994e6dd7ed3094f44639e706a57231f031f0e52186b6
7
+ data.tar.gz: dc44b6a2f0e5d21bdcbe156f0cc94c324d586c74e9fb757454157ac014897fc3cbd683acb19e6397d668f67a9c68e97d683521f1ceecdbf338e24f3d497c688d
checksums.yaml.gz.sig CHANGED
Binary file
@@ -148,6 +148,11 @@ jobs:
148
148
 
149
149
  bundle config set without 'tools benchmarks docs'
150
150
 
151
+ - name: Fix directory permissions for Bundler
152
+ run: |
153
+ chmod -R o-w /opt/hostedtoolcache/Ruby/3*/x64/lib/ruby/gems/3*/gems
154
+ chmod +t /opt/hostedtoolcache/Ruby/3*/x64/lib/ruby/gems/3*/gems
155
+
151
156
  - name: Bundle install
152
157
  run: |
153
158
  bundle config set without development
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.3
1
+ 3.3.4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Karafka framework changelog
2
2
 
3
+ ## 2.4.5 (2024-07-18)
4
+ - [Change] Inject `client.id` when building subscription group and not during the initial setup.
5
+ - [Fix] Mitigate `confluentinc/librdkafka/issues/4783` by injecting dynamic client id when using `cooperative-sticky` strategy.
6
+
7
+ ### Change Note
8
+
9
+ `client.id` is technically a low-importance value that should not (aside from this error) impact operations. This is why it is not considered a breaking change. This change may be reverted when the original issue is fixed in librdkafka.
10
+
11
+ ## 2.4.4 (2024-07-04)
12
+ - [Enhancement] Allow for offset storing from the Filtering API.
13
+ - [Enhancement] Print more extensive error info on forceful shutdown.
14
+ - [Enhancement] Include `original_key` in the DLQ dispatch headers.
15
+ - [Enhancement] Support embedding mode control management from the trap context.
16
+ - [Enhancement] Make sure, that the listener thread is stopped before restarting.
17
+ - [Fix] Do not block on hanging listener shutdown when invoking forceful shutdown.
18
+ - [Fix] Static membership fencing error is not propagated explicitly enough.
19
+ - [Fix] Make sure DLQ dispatches raw headers and not deserialized headers (same as payload).
20
+ - [Fix] Fix a typo where `ms` in logger listener would not have space before it.
21
+ - [Maintenance] Require `karafka-core` `>=` `2.4.3`.
22
+ - [Maintenance] Allow for usage of `karafka-rdkafka` `~` `0.16` to support librdkafka `2.4.0`.
23
+ - [Maintenance] Lower the precision reporting to 100 microseconds in the logger listener.
24
+
3
25
  ## 2.4.3 (2024-06-12)
4
26
  - [Enhancement] Allow for customization of Virtual Partitions reducer for enhanced parallelization.
5
27
  - [Enhancement] Add more error codes to early report on polling issues (kidlab)
data/Gemfile.lock CHANGED
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- karafka (2.4.3)
4
+ karafka (2.4.5)
5
5
  base64 (~> 0.2)
6
- karafka-core (>= 2.4.0, < 2.5.0)
6
+ karafka-core (>= 2.4.3, < 2.5.0)
7
7
  waterdrop (>= 2.7.3, < 3.0.0)
8
8
  zeitwerk (~> 2.3)
9
9
 
@@ -26,12 +26,12 @@ GEM
26
26
  base64 (0.2.0)
27
27
  bigdecimal (3.1.8)
28
28
  byebug (11.1.3)
29
- concurrent-ruby (1.3.1)
29
+ concurrent-ruby (1.3.3)
30
30
  connection_pool (2.4.1)
31
31
  diff-lcs (1.5.1)
32
32
  docile (1.4.0)
33
33
  drb (2.2.1)
34
- erubi (1.12.0)
34
+ erubi (1.13.0)
35
35
  factory_bot (6.4.6)
36
36
  activesupport (>= 5.0.0)
37
37
  ffi (1.17.0)
@@ -39,13 +39,13 @@ GEM
39
39
  activesupport (>= 6.1)
40
40
  i18n (1.14.5)
41
41
  concurrent-ruby (~> 1.0)
42
- karafka-core (2.4.0)
43
- karafka-rdkafka (>= 0.15.0, < 0.16.0)
44
- karafka-rdkafka (0.15.1)
42
+ karafka-core (2.4.3)
43
+ karafka-rdkafka (>= 0.15.0, < 0.17.0)
44
+ karafka-rdkafka (0.16.0)
45
45
  ffi (~> 1.15)
46
46
  mini_portile2 (~> 2.6)
47
47
  rake (> 12)
48
- karafka-testing (2.4.3)
48
+ karafka-testing (2.4.4)
49
49
  karafka (>= 2.4.0, < 2.5.0)
50
50
  waterdrop (>= 2.7.0)
51
51
  karafka-web (0.9.1)
@@ -55,12 +55,12 @@ GEM
55
55
  roda (~> 3.68, >= 3.69)
56
56
  tilt (~> 2.0)
57
57
  mini_portile2 (2.8.7)
58
- minitest (5.23.1)
58
+ minitest (5.24.0)
59
59
  mutex_m (0.2.0)
60
60
  ostruct (0.6.0)
61
- rack (3.0.11)
61
+ rack (3.1.4)
62
62
  rake (13.2.1)
63
- roda (3.80.0)
63
+ roda (3.81.0)
64
64
  rack
65
65
  rspec (3.13.0)
66
66
  rspec-core (~> 3.13.0)
@@ -68,7 +68,7 @@ GEM
68
68
  rspec-mocks (~> 3.13.0)
69
69
  rspec-core (3.13.0)
70
70
  rspec-support (~> 3.13.0)
71
- rspec-expectations (3.13.0)
71
+ rspec-expectations (3.13.1)
72
72
  diff-lcs (>= 1.2.0, < 2.0)
73
73
  rspec-support (~> 3.13.0)
74
74
  rspec-mocks (3.13.1)
@@ -81,14 +81,14 @@ GEM
81
81
  simplecov_json_formatter (~> 0.1)
82
82
  simplecov-html (0.12.3)
83
83
  simplecov_json_formatter (0.1.4)
84
- tilt (2.3.0)
84
+ tilt (2.4.0)
85
85
  tzinfo (2.0.6)
86
86
  concurrent-ruby (~> 1.0)
87
87
  waterdrop (2.7.3)
88
88
  karafka-core (>= 2.4.0, < 3.0.0)
89
89
  karafka-rdkafka (>= 0.15.1)
90
90
  zeitwerk (~> 2.3)
91
- zeitwerk (2.6.15)
91
+ zeitwerk (2.6.16)
92
92
 
93
93
  PLATFORMS
94
94
  ruby
@@ -106,4 +106,4 @@ DEPENDENCIES
106
106
  simplecov
107
107
 
108
108
  BUNDLED WITH
109
- 2.5.11
109
+ 2.5.14
data/docker-compose.yml CHANGED
@@ -3,7 +3,7 @@ version: '2'
3
3
  services:
4
4
  kafka:
5
5
  container_name: kafka
6
- image: confluentinc/cp-kafka:7.6.1
6
+ image: confluentinc/cp-kafka:7.6.2
7
7
 
8
8
  ports:
9
9
  - 9092:9092
data/karafka.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  DESC
23
23
 
24
24
  spec.add_dependency 'base64', '~> 0.2'
25
- spec.add_dependency 'karafka-core', '>= 2.4.0', '< 2.5.0'
25
+ spec.add_dependency 'karafka-core', '>= 2.4.3', '< 2.5.0'
26
26
  spec.add_dependency 'waterdrop', '>= 2.7.3', '< 3.0.0'
27
27
  spec.add_dependency 'zeitwerk', '~> 2.3'
28
28
 
@@ -559,6 +559,9 @@ module Karafka
559
559
  early_report = true
560
560
  when :cluster_authorization_failed # 31
561
561
  early_report = true
562
+ # This can happen for many reasons, including issues with static membership being fenced
563
+ when :fatal # -150
564
+ early_report = true
562
565
  # @see
563
566
  # https://github.com/confluentinc/confluent-kafka-dotnet/issues/1366#issuecomment-821842990
564
567
  # This will be raised each time poll detects a non-existing topic. When auto creation is
@@ -2,11 +2,28 @@
2
2
 
3
3
  module Karafka
4
4
  # Allows to start and stop Karafka as part of a different process
5
+ # Following limitations and restrictions apply:
6
+ #
7
+ # - `#start` cannot be called from a trap context - non blocking
8
+ # - `#quiet` - can be called from a trap context - non blocking
9
+ # - `#stop` - can be called from a trap context - blocking
5
10
  module Embedded
6
11
  class << self
12
+ # Lock for ensuring we do not control embedding in parallel
13
+ MUTEX = Mutex.new
14
+
15
+ private_constant :MUTEX
16
+
7
17
  # Starts Karafka without supervision and without ownership of signals in a background thread
8
18
  # so it won't interrupt other things running
9
19
  def start
20
+ MUTEX.synchronize do
21
+ # Prevent from double-starting
22
+ return if @started
23
+
24
+ @started = true
25
+ end
26
+
10
27
  Thread.new do
11
28
  Thread.current.name = 'karafka.embedded'
12
29
 
@@ -19,9 +36,32 @@ module Karafka
19
36
  #
20
37
  # @note This method is blocking because we want to wait until Karafka is stopped with final
21
38
  # process shutdown
39
+ #
40
+ # @note This method **is** safe to run from a trap context.
22
41
  def stop
23
- # Stop needs to be blocking to wait for all the things to finalize
24
- Karafka::Server.stop
42
+ # Prevent from double stopping
43
+ unless @stopping
44
+ Thread.new do
45
+ Thread.current.name = 'karafka.embedded.stopping'
46
+
47
+ stop = false
48
+
49
+ # We spawn a new thread because `#stop` may be called from a trap context
50
+ MUTEX.synchronize do
51
+ break if @stopping
52
+
53
+ @stopping = true
54
+ stop = true
55
+ end
56
+
57
+ next unless stop
58
+
59
+ Karafka::Server.stop
60
+ end
61
+ end
62
+
63
+ # Since we want to have this blocking, we wait for the background thread
64
+ sleep(0.1) until Karafka::App.terminated?
25
65
  end
26
66
 
27
67
  # Quiets Karafka upon any event
@@ -29,6 +69,8 @@ module Karafka
29
69
  # @note This method is not blocking and will not wait for Karafka to fully quiet.
30
70
  # It will trigger the quiet procedure but won't wait.
31
71
  #
72
+ # @note This method **can** be called from a trap context.
73
+ #
32
74
  # @note Please keep in mind you need to `#stop` to actually stop the server anyhow.
33
75
  def quiet
34
76
  Karafka::Server.quiet
@@ -22,7 +22,16 @@ module Karafka
22
22
  def included(base)
23
23
  base.extend ::Forwardable
24
24
 
25
- base.def_delegators :@thread, :join, :terminate, :alive?, :name
25
+ base.def_delegators :@thread, :join, :terminate, :name
26
+ end
27
+ end
28
+
29
+ # @return [Boolean] true if thread is present and is running, false otherwise
30
+ def alive?
31
+ MUTEX.synchronize do
32
+ return false unless @thread
33
+
34
+ @thread.alive?
26
35
  end
27
36
  end
28
37
 
@@ -41,7 +41,7 @@ module Karafka
41
41
  return unless log_polling?
42
42
 
43
43
  listener = event[:caller]
44
- time = event[:time]
44
+ time = event[:time].round(2)
45
45
  messages_count = event[:messages_buffer].size
46
46
 
47
47
  message = "[#{listener.id}] Polled #{messages_count} messages in #{time}ms"
@@ -69,14 +69,14 @@ module Karafka
69
69
  # @param event [Karafka::Core::Monitoring::Event] event details including payload
70
70
  def on_worker_processed(event)
71
71
  job = event[:job]
72
- time = event[:time]
72
+ time = event[:time].round(2)
73
73
  job_type = job.class.to_s.split('::').last
74
74
  consumer = job.executor.topic.consumer
75
75
  topic = job.executor.topic.name
76
76
  partition = job.executor.partition
77
77
  info <<~MSG.tr("\n", ' ').strip!
78
78
  [#{job.id}] #{job_type} job for #{consumer}
79
- on #{topic}/#{partition} finished in #{time}ms
79
+ on #{topic}/#{partition} finished in #{time} ms
80
80
  MSG
81
81
  end
82
82
 
@@ -306,7 +306,24 @@ module Karafka
306
306
  fatal "Runner crashed due to an error: #{error}"
307
307
  fatal details
308
308
  when 'app.stopping.error'
309
- error 'Forceful Karafka server stop'
309
+ # Counts number of workers and listeners that were still active when forcing the
310
+ # shutdown. Please note, that unless all listeners are closed, workers will not finalize
311
+ # their operations as well.
312
+ # We need to check if listeners and workers are assigned as during super early stages of
313
+ # boot they are not.
314
+ listeners = Server.listeners ? Server.listeners.count(&:active?) : 0
315
+ workers = Server.workers ? Server.workers.count(&:alive?) : 0
316
+
317
+ message = <<~MSG.tr("\n", ' ').strip!
318
+ Forceful Karafka server stop with:
319
+ #{workers} active workers and
320
+ #{listeners} active listeners
321
+ MSG
322
+
323
+ error message
324
+ when 'app.forceful_stopping.error'
325
+ error "Forceful shutdown error occurred: #{error}"
326
+ error details
310
327
  when 'librdkafka.error'
311
328
  error "librdkafka internal error occurred: #{error}"
312
329
  error details
@@ -216,6 +216,10 @@ module Karafka
216
216
  next unless multi_part_sgs_families.include?(sg_listener.subscription_group.name)
217
217
  # Skip already active connections
218
218
  next unless sg_listener.pending? || sg_listener.stopped?
219
+ # Ensure that the listener thread under which we operate is already stopped and
220
+ # is not dangling. While not likely to happen, this may protect against a
221
+ # case where a shutdown critical crash would case a restart of the same listener
222
+ next if sg_listener.alive?
219
223
 
220
224
  touch(sg_listener.subscription_group.id)
221
225
  sg_listener.start!
@@ -89,6 +89,23 @@ module Karafka
89
89
  applied.map(&:cursor).compact.min_by(&:offset)
90
90
  end
91
91
 
92
+ # @return [Boolean] did any of the filters requested offset storage during filter
93
+ # application
94
+ def mark_as_consumed?
95
+ # We can manage filtering offset only when user wanted that and there is a cursor
96
+ # to use
97
+ applied.any?(&:mark_as_consumed?) && cursor
98
+ end
99
+
100
+ # @return [Symbol] `:mark_as_consumed` or `:mark_as_consumed!`
101
+ def marking_method
102
+ candidates = applied.map(&:marking_method)
103
+
104
+ return :mark_as_consumed! if candidates.include?(:mark_as_consumed!)
105
+
106
+ :mark_as_consumed
107
+ end
108
+
92
109
  private
93
110
 
94
111
  # @return [Boolean] is filtering active
@@ -54,6 +54,18 @@ module Karafka
54
54
  def timeout
55
55
  0
56
56
  end
57
+
58
+ # @return [Boolean] should we use the cursor value to mark as consumed. If any of the
59
+ # filters returns true, we return lowers applicable cursor value (if any)
60
+ def mark_as_consumed?
61
+ false
62
+ end
63
+
64
+ # @return [Symbol] `:mark_as_consumed` or `:mark_as_consumed!`. Applicable only if
65
+ # marking is requested
66
+ def marking_method
67
+ :mark_as_consumed
68
+ end
57
69
  end
58
70
  end
59
71
  end
@@ -33,7 +33,7 @@ module Karafka
33
33
 
34
34
  # Time on message is in seconds with ms precision, so we need to convert the ttl that
35
35
  # is in ms to this format
36
- border = ::Time.now.utc - @delay / 1_000.to_f
36
+ border = ::Time.now.utc - @delay / 1_000.0
37
37
 
38
38
  messages.delete_if do |message|
39
39
  too_young = message.timestamp > border
@@ -52,7 +52,7 @@ module Karafka
52
52
  def timeout
53
53
  return 0 unless @cursor
54
54
 
55
- timeout = (@delay / 1_000.to_f) - (::Time.now.utc - @cursor.timestamp)
55
+ timeout = (@delay / 1_000.0) - (::Time.now.utc - @cursor.timestamp)
56
56
 
57
57
  timeout <= 0 ? 0 : timeout * 1_000
58
58
  end
@@ -154,11 +154,12 @@ module Karafka
154
154
  topic: topic.dead_letter_queue.topic,
155
155
  key: original_partition,
156
156
  payload: skippable_message.raw_payload,
157
- headers: skippable_message.headers.merge(
157
+ headers: skippable_message.raw_headers.merge(
158
158
  'original_topic' => topic.name,
159
159
  'original_partition' => original_partition,
160
160
  'original_offset' => skippable_message.offset.to_s,
161
161
  'original_consumer_group' => topic.consumer_group.id,
162
+ 'original_key' => skippable_message.raw_key.to_s,
162
163
  'original_attempts' => attempt.to_s
163
164
  )
164
165
  }
@@ -66,6 +66,19 @@ module Karafka
66
66
  # and this should not happen
67
67
  throttle_timeout = filter.timeout
68
68
 
69
+ # If user requested marking when applying filter, we mark. We may be in the user
70
+ # flow but even then this is not a problem. Older offsets will be ignored since
71
+ # we do not force the offset update (expected) and newer are on the user to control.
72
+ # This can be primarily used when filtering large quantities of data to mark on the
73
+ # idle runs, so lag reporting is aware that those messages were not consumed but also
74
+ # are no longer relevant
75
+ if filter.mark_as_consumed?
76
+ send(
77
+ filter.marking_method,
78
+ filter.cursor
79
+ )
80
+ end
81
+
69
82
  case filter.action
70
83
  when :skip
71
84
  nil
@@ -121,8 +121,8 @@ module Karafka
121
121
  kafka = Setup::AttributesMap.consumer(@topics.first.kafka.dup)
122
122
 
123
123
  inject_group_instance_id(kafka)
124
+ inject_client_id(kafka)
124
125
 
125
- kafka[:'client.id'] ||= client_id
126
126
  kafka[:'group.id'] ||= @consumer_group.id
127
127
  kafka[:'auto.offset.reset'] ||= @topics.first.initial_offset
128
128
  # Karafka manages the offsets based on the processing state, thus we do not rely on the
@@ -132,6 +132,26 @@ module Karafka
132
132
  kafka
133
133
  end
134
134
 
135
+ # Sets (if needed) the client.id attribute
136
+ #
137
+ # @param kafka [Hash] kafka level config
138
+ def inject_client_id(kafka)
139
+ # If client id is set directly on librdkafka level, we do nothing and just go with what
140
+ # end user has configured
141
+ return if kafka.key?(:'client.id')
142
+
143
+ # This mitigates an issue for multiplexing and potentially other cases when running
144
+ # multiple karafka processes on one machine, where librdkafka goes into an infinite
145
+ # loop when using cooperative-sticky and upscaling.
146
+ #
147
+ # @see https://github.com/confluentinc/librdkafka/issues/4783
148
+ kafka[:'client.id'] = if kafka[:'partition.assignment.strategy'] == 'cooperative-sticky'
149
+ "#{client_id}/#{Time.now.to_f}/#{SecureRandom.hex[0..9]}"
150
+ else
151
+ client_id
152
+ end
153
+ end
154
+
135
155
  # If we use static group memberships, there can be a case, where same instance id would
136
156
  # be set on many subscription groups as the group instance id from Karafka perspective is
137
157
  # set per config. Each instance even if they are subscribed to different topics needs to
@@ -3,6 +3,12 @@
3
3
  module Karafka
4
4
  # Karafka consuming server class
5
5
  class Server
6
+ # How long should we wait on the listeners forceful shutdown when they are stuck beyond the
7
+ # shutdown timeout before forcing a bypass
8
+ FORCEFUL_SHUTDOWN_WAIT = 5
9
+
10
+ private_constant :FORCEFUL_SHUTDOWN_WAIT
11
+
6
12
  class << self
7
13
  # Set of consuming threads. Each consumer thread contains a single consumer
8
14
  attr_accessor :listeners
@@ -105,9 +111,23 @@ module Karafka
105
111
  # We're done waiting, lets kill them!
106
112
  workers.each(&:terminate)
107
113
  listeners.active.each(&:terminate)
114
+
108
115
  # We always need to shutdown clients to make sure we do not force the GC to close consumer.
109
116
  # This can cause memory leaks and crashes.
110
- listeners.each(&:shutdown)
117
+ # We run it in a separate thread in case this would hang and we ignore it after the time
118
+ # we assigned to it and force shutdown as we prefer to stop the process rather than wait
119
+ # indefinitely even with risk of VM crash as this is a last resort.
120
+ Thread.new do
121
+ listeners.each(&:shutdown)
122
+ rescue StandardError => e
123
+ # If anything wrong happened during shutdown, we also want to record it
124
+ Karafka.monitor.instrument(
125
+ 'error.occurred',
126
+ caller: self,
127
+ error: e,
128
+ type: 'app.forceful_stopping.error'
129
+ )
130
+ end.join(FORCEFUL_SHUTDOWN_WAIT)
111
131
 
112
132
  # We also do not forcefully terminate everything when running in the embedded mode,
113
133
  # otherwise we would overwrite the shutdown process of the process that started Karafka
@@ -52,7 +52,9 @@ module Karafka
52
52
  fetch.wait.max.ms
53
53
  group.id
54
54
  group.instance.id
55
+ group.protocol
55
56
  group.protocol.type
57
+ group.remote.assignor
56
58
  heartbeat.interval.ms
57
59
  interceptors
58
60
  internal.termination.signal
@@ -385,9 +385,6 @@ module Karafka
385
385
  config.kafka[key] = value
386
386
  end
387
387
 
388
- # Use Karafka client_id as kafka client id if not set
389
- config.kafka[:'client.id'] ||= config.client_id
390
-
391
388
  return if Karafka::App.env.production?
392
389
 
393
390
  KAFKA_DEV_DEFAULTS.each do |key, value|
@@ -3,5 +3,5 @@
3
3
  # Main module namespace
4
4
  module Karafka
5
5
  # Current Karafka version
6
- VERSION = '2.4.3'
6
+ VERSION = '2.4.5'
7
7
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: karafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.3
4
+ version: 2.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -35,7 +35,7 @@ cert_chain:
35
35
  AnG1dJU+yL2BK7vaVytLTstJME5mepSZ46qqIJXMuWob/YPDmVaBF39TDSG9e34s
36
36
  msG3BiCqgOgHAnL23+CN3Rt8MsuRfEtoTKpJVcCfoEoNHOkc
37
37
  -----END CERTIFICATE-----
38
- date: 2024-06-12 00:00:00.000000000 Z
38
+ date: 2024-07-18 00:00:00.000000000 Z
39
39
  dependencies:
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: base64
@@ -57,7 +57,7 @@ dependencies:
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 2.4.0
60
+ version: 2.4.3
61
61
  - - "<"
62
62
  - !ruby/object:Gem::Version
63
63
  version: 2.5.0
@@ -67,7 +67,7 @@ dependencies:
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- version: 2.4.0
70
+ version: 2.4.3
71
71
  - - "<"
72
72
  - !ruby/object:Gem::Version
73
73
  version: 2.5.0
metadata.gz.sig CHANGED
Binary file