sidekiq 7.3.2 → 7.3.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: ce4653de61d5b923ce7556490e95cf6918dd1c94340417ec4093f11cc8ceda39
4
- data.tar.gz: e9faa23d4535a5b647c5dc86c28ce64317ea964f44faa9141b5a883f77b80dcd
3
+ metadata.gz: ada259b22664fbbb740c3d28fa06578cb6fc00a42590ea459a1f2395f8e38a92
4
+ data.tar.gz: 0636466a99e8a0df4d955b0928d76b504df74bba4e3ab13a2d08237ccd57b162
5
5
  SHA512:
6
- metadata.gz: 5fa259f415261689936b4f03f179198ea6849fbe63b932b5836fa800714055ce574a80a56462355a92d125440362957ebf02e5bafbde55fe8211805c80573ae5
7
- data.tar.gz: 0aaf341ae13c377e6f944344e0feef246e3450ae9eb0d74fcbf8e932e9c9fcf0d8f0dd56ce374064d4f51c4d99f4c306560c3283db27f70ae6aa5af346d83ec3
6
+ metadata.gz: 209f76b8ceebdb2cac83d22aab78873dad373a9ba488e7d320972e8ced4d3ac71454ef6669db321727ce3e4a85fdc5744005ff64d1a0f4cd48d9a4231bddf384
7
+ data.tar.gz: df094fd4baa169249e92309814d67d0fb5a53b5da4990c0510f5ae1d96e07541356a3014f75b8b79a97f0a91b03b92b576331422c54f36bb2c4943f2f0f87b08
data/Changes.md CHANGED
@@ -2,12 +2,50 @@
2
2
 
3
3
  [Sidekiq Changes](https://github.com/sidekiq/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/sidekiq/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/sidekiq/sidekiq/blob/main/Ent-Changes.md)
4
4
 
5
+ 7.3.5
6
+ ----------
7
+
8
+ - Reimplement `retry_all` and `kill_all` API methods to use ZPOPMIN,
9
+ approximately 30-60% faster. [#6481]
10
+ - Add preload testing binary at `examples/testing/sidekiq_boot` to verify your Rails app boots correctly with Sidekiq Enterprise's app preloading.
11
+ - Fix circular require with ActiveJob adapter [#6477]
12
+ - Fix potential race condition leading to incorrect serialized values for CurrentAttributes [#6475]
13
+ - Restore missing elapsed time when default job logging is disabled
14
+
15
+ 7.3.4
16
+ ----------
17
+
18
+ - Fix FrozenError when starting Sidekiq [#6470]
19
+
20
+ 7.3.3
21
+ ----------
22
+
23
+ - Freeze global configuration once boot is complete, to avoid configuration race conditions [#6466, #6465]
24
+ - Sidekiq now warns if a job iteration takes longer than the `-t` timeout setting (defaults to 25 seconds)
25
+ - Iteration callbacks now have easy access to job arguments via the `arguments` method:
26
+ ```ruby
27
+ def on_stop
28
+ p arguments # => `[123, "string", {"key" => "value"}]`
29
+ id, str, hash = arguments
30
+ end
31
+ ```
32
+ - Iterable jobs can be cancelled via `Sidekiq::Client#cancel!`:
33
+ ```ruby
34
+ c = Sidekiq::Client.new
35
+ jid = c.push("class" => SomeJob, "args" => [123])
36
+ c.cancel!(jid) # => true
37
+ ```
38
+ - Take over support for ActiveJob's `:sidekiq` adapter [#6430, fatkodima]
39
+ - Ensure CurrentAttributes are in scope when creating batch callbacks [#6455]
40
+ - Add `Sidekiq.gem_version` API.
41
+ - Update Ukranian translations
42
+
5
43
  7.3.2
6
44
  ----------
7
45
 
8
46
  - Adjust ActiveRecord batch iteration to restart an interrupted batch from the beginning.
9
47
  Each batch should be processed as a single transaction in order to be idempotent. [#6405]
10
- - Fix typo in S::DeadSet#kill [#6397]
48
+ - Fix typo in Sidekiq::DeadSet#kill [#6397]
11
49
  - Fix CSS issue with bottom bar in Web UI [#6414]
12
50
 
13
51
  7.3.1
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module ActiveJob
5
+ # @api private
6
+ class Wrapper
7
+ include Sidekiq::Job
8
+
9
+ def perform(job_data)
10
+ ::ActiveJob::Base.execute(job_data.merge("provider_job_id" => jid))
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module ActiveJob
17
+ module QueueAdapters
18
+ # Explicitly remove the implementation existing in older rails'.
19
+ remove_const(:SidekiqAdapter) if defined?("::#{name}::SidekiqAdapter")
20
+
21
+ # Sidekiq adapter for Active Job
22
+ #
23
+ # To use Sidekiq set the queue_adapter config to +:sidekiq+.
24
+ #
25
+ # Rails.application.config.active_job.queue_adapter = :sidekiq
26
+ class SidekiqAdapter
27
+ # Defines whether enqueuing should happen implicitly to after commit when called
28
+ # from inside a transaction.
29
+ # @api private
30
+ def enqueue_after_transaction_commit?
31
+ true
32
+ end
33
+
34
+ # @api private
35
+ def enqueue(job)
36
+ job.provider_job_id = JobWrapper.set(
37
+ wrapped: job.class,
38
+ queue: job.queue_name
39
+ ).perform_async(job.serialize)
40
+ end
41
+
42
+ # @api private
43
+ def enqueue_at(job, timestamp)
44
+ job.provider_job_id = JobWrapper.set(
45
+ wrapped: job.class,
46
+ queue: job.queue_name
47
+ ).perform_at(timestamp, job.serialize)
48
+ end
49
+
50
+ # @api private
51
+ def enqueue_all(jobs)
52
+ enqueued_count = 0
53
+ jobs.group_by(&:class).each do |job_class, same_class_jobs|
54
+ same_class_jobs.group_by(&:queue_name).each do |queue, same_class_and_queue_jobs|
55
+ immediate_jobs, scheduled_jobs = same_class_and_queue_jobs.partition { |job| job.scheduled_at.nil? }
56
+
57
+ if immediate_jobs.any?
58
+ jids = Sidekiq::Client.push_bulk(
59
+ "class" => JobWrapper,
60
+ "wrapped" => job_class,
61
+ "queue" => queue,
62
+ "args" => immediate_jobs.map { |job| [job.serialize] }
63
+ )
64
+ enqueued_count += jids.compact.size
65
+ end
66
+
67
+ if scheduled_jobs.any?
68
+ jids = Sidekiq::Client.push_bulk(
69
+ "class" => JobWrapper,
70
+ "wrapped" => job_class,
71
+ "queue" => queue,
72
+ "args" => scheduled_jobs.map { |job| [job.serialize] },
73
+ "at" => scheduled_jobs.map { |job| job.scheduled_at&.to_f }
74
+ )
75
+ enqueued_count += jids.compact.size
76
+ end
77
+ end
78
+ end
79
+ enqueued_count
80
+ end
81
+
82
+ # Defines a class alias for backwards compatibility with enqueued Active Job jobs.
83
+ # @api private
84
+ class JobWrapper < Sidekiq::ActiveJob::Wrapper
85
+ end
86
+ end
87
+ end
88
+ end
data/lib/sidekiq/api.rb CHANGED
@@ -373,7 +373,7 @@ module Sidekiq
373
373
  def display_class
374
374
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
375
375
  @klass ||= self["display_class"] || begin
376
- if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
376
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
377
377
  job_class = @item["wrapped"] || args[0]
378
378
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
379
379
  # MailerClass#mailer_method
@@ -389,7 +389,7 @@ module Sidekiq
389
389
 
390
390
  def display_args
391
391
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
392
- @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
392
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
393
393
  job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
394
394
  if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
395
395
  # remove MailerClass, mailer_method and 'deliver_now'
@@ -668,6 +668,41 @@ module Sidekiq
668
668
  end
669
669
  end
670
670
 
671
+ def pop_each
672
+ Sidekiq.redis do |c|
673
+ size.times do
674
+ data, score = c.zpopmin(name, 1)&.first
675
+ break unless data
676
+ yield data, score
677
+ end
678
+ end
679
+ end
680
+
681
+ def retry_all
682
+ c = Sidekiq::Client.new
683
+ pop_each do |msg, _|
684
+ job = Sidekiq.load_json(msg)
685
+ # Manual retries should not count against the retry limit.
686
+ job["retry_count"] -= 1 if job["retry_count"]
687
+ c.push(job)
688
+ end
689
+ end
690
+
691
+ # Move all jobs from this Set to the Dead Set.
692
+ # See DeadSet#kill
693
+ def kill_all(notify_failure: false, ex: nil)
694
+ ds = DeadSet.new
695
+ opts = {notify_failure: notify_failure, ex: ex, trim: false}
696
+
697
+ begin
698
+ pop_each do |msg, _|
699
+ ds.kill(msg, opts)
700
+ end
701
+ ensure
702
+ ds.trim
703
+ end
704
+ end
705
+
671
706
  def each
672
707
  initial_size = @_size
673
708
  offset_size = 0
@@ -765,10 +800,6 @@ module Sidekiq
765
800
 
766
801
  ##
767
802
  # The set of scheduled jobs within Sidekiq.
768
- # Based on this, you can search/filter for jobs. Here's an
769
- # example where I'm selecting jobs based on some complex logic
770
- # and deleting them from the scheduled set.
771
- #
772
803
  # See the API wiki page for usage notes and examples.
773
804
  #
774
805
  class ScheduledSet < JobSet
@@ -779,26 +810,12 @@ module Sidekiq
779
810
 
780
811
  ##
781
812
  # The set of retries within Sidekiq.
782
- # Based on this, you can search/filter for jobs. Here's an
783
- # example where I'm selecting all jobs of a certain type
784
- # and deleting them from the retry queue.
785
- #
786
813
  # See the API wiki page for usage notes and examples.
787
814
  #
788
815
  class RetrySet < JobSet
789
816
  def initialize
790
817
  super("retry")
791
818
  end
792
-
793
- # Enqueues all jobs pending within the retry set.
794
- def retry_all
795
- each(&:retry) while size > 0
796
- end
797
-
798
- # Kills all jobs pending within the retry set.
799
- def kill_all
800
- each(&:kill) while size > 0
801
- end
802
819
  end
803
820
 
804
821
  ##
@@ -811,20 +828,31 @@ module Sidekiq
811
828
  super("dead")
812
829
  end
813
830
 
831
+ # Trim dead jobs which are over our storage limits
832
+ def trim
833
+ hash = Sidekiq.default_configuration
834
+ now = Time.now.to_f
835
+ Sidekiq.redis do |conn|
836
+ conn.multi do |transaction|
837
+ transaction.zremrangebyscore(name, "-inf", now - hash[:dead_timeout_in_seconds])
838
+ transaction.zremrangebyrank(name, 0, - hash[:dead_max_jobs])
839
+ end
840
+ end
841
+ end
842
+
814
843
  # Add the given job to the Dead set.
815
844
  # @param message [String] the job data as JSON
816
- # @option opts [Boolean] :notify_failure (true) Whether death handlers should be called
845
+ # @option opts [Boolean] :notify_failure (true) Whether death handlers should be called
846
+ # @option opts [Boolean] :trim (true) Whether Sidekiq should trim the structure to keep it within configuration
817
847
  # @option opts [Exception] :ex (RuntimeError) An exception to pass to the death handlers
818
848
  def kill(message, opts = {})
819
849
  now = Time.now.to_f
820
850
  Sidekiq.redis do |conn|
821
- conn.multi do |transaction|
822
- transaction.zadd(name, now.to_s, message)
823
- transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
824
- transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
825
- end
851
+ conn.zadd(name, now.to_s, message)
826
852
  end
827
853
 
854
+ trim if opts[:trim] != false
855
+
828
856
  if opts[:notify_failure] != false
829
857
  job = Sidekiq.load_json(message)
830
858
  if opts[:ex]
@@ -839,11 +867,6 @@ module Sidekiq
839
867
  end
840
868
  true
841
869
  end
842
-
843
- # Enqueue all dead jobs
844
- def retry_all
845
- each(&:retry) while size > 0
846
- end
847
870
  end
848
871
 
849
872
  ##
@@ -40,9 +40,9 @@ module Sidekiq
40
40
 
41
41
  def fetcher
42
42
  @fetcher ||= begin
43
- inst = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
44
- inst.setup(config[:fetch_setup]) if inst.respond_to?(:setup)
45
- inst
43
+ instance = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
44
+ instance.setup(config[:fetch_setup]) if instance.respond_to?(:setup)
45
+ instance
46
46
  end
47
47
  end
48
48
 
@@ -58,6 +58,23 @@ module Sidekiq
58
58
  end
59
59
  end
60
60
 
61
+ # Cancel the IterableJob with the given JID.
62
+ # **NB: Cancellation is asynchronous.** Iteration checks every
63
+ # five seconds so this will not immediately stop the given job.
64
+ def cancel!(jid)
65
+ key = "it-#{jid}"
66
+ _, result, _ = Sidekiq.redis do |c|
67
+ c.pipelined do |p|
68
+ p.hsetnx(key, "cancelled", Time.now.to_i)
69
+ p.hget(key, "cancelled")
70
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
71
+ # TODO When Redis 7.2 is required
72
+ # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
73
+ end
74
+ end
75
+ result.to_i
76
+ end
77
+
61
78
  ##
62
79
  # The main method used to push a job to Redis. Accepts a number of options:
63
80
  #
@@ -185,7 +185,13 @@ module Sidekiq
185
185
 
186
186
  # register global singletons which can be accessed elsewhere
187
187
  def register(name, instance)
188
- @directory[name] = instance
188
+ # logger.debug("register[#{name}] = #{instance}")
189
+ # Sidekiq Enterprise lazy registers a few services so we
190
+ # can't lock down this hash completely.
191
+ hash = @directory.dup
192
+ hash[name] = instance
193
+ @directory = hash.freeze
194
+ instance
189
195
  end
190
196
 
191
197
  # find a singleton
@@ -193,10 +199,16 @@ module Sidekiq
193
199
  # JNDI is just a fancy name for a hash lookup
194
200
  @directory.fetch(name) do |key|
195
201
  return nil unless default_class
196
- @directory[key] = default_class.new(self)
202
+ register(key, default_class.new(self))
197
203
  end
198
204
  end
199
205
 
206
+ def freeze!
207
+ @directory.freeze
208
+ @options.freeze
209
+ true
210
+ end
211
+
200
212
  ##
201
213
  # Death handlers are called when all retries for a job have been exhausted and
202
214
  # the job dies. It's the notification to your application
@@ -30,6 +30,40 @@ module Sidekiq
30
30
  @_cursor = nil
31
31
  @_start_time = nil
32
32
  @_runtime = 0
33
+ @_args = nil
34
+ @_cancelled = nil
35
+ end
36
+
37
+ def arguments
38
+ @_args
39
+ end
40
+
41
+ # Three days is the longest period you generally need to wait for a retry to
42
+ # execute when using the default retry scheme. We don't want to "forget" the job
43
+ # is cancelled before it has a chance to execute and cancel itself.
44
+ CANCELLATION_PERIOD = (3 * 86_400).to_s
45
+
46
+ # Set a flag in Redis to mark this job as cancelled.
47
+ # Cancellation is asynchronous and is checked at the start of iteration
48
+ # and every 5 seconds thereafter as part of the recurring state flush.
49
+ def cancel!
50
+ return @_cancelled if cancelled?
51
+
52
+ key = "it-#{jid}"
53
+ _, result, _ = Sidekiq.redis do |c|
54
+ c.pipelined do |p|
55
+ p.hsetnx(key, "cancelled", Time.now.to_i)
56
+ p.hget(key, "cancelled")
57
+ # TODO When Redis 7.2 is required
58
+ # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
59
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
60
+ end
61
+ end
62
+ @_cancelled = result.to_i
63
+ end
64
+
65
+ def cancelled?
66
+ @_cancelled
33
67
  end
34
68
 
35
69
  # A hook to override that will be called when the job starts iterating.
@@ -91,13 +125,14 @@ module Sidekiq
91
125
  end
92
126
 
93
127
  # @api private
94
- def perform(*arguments)
128
+ def perform(*args)
129
+ @_args = args.dup.freeze
95
130
  fetch_previous_iteration_state
96
131
 
97
132
  @_executions += 1
98
133
  @_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
99
134
 
100
- enumerator = build_enumerator(*arguments, cursor: @_cursor)
135
+ enumerator = build_enumerator(*args, cursor: @_cursor)
101
136
  unless enumerator
102
137
  logger.info("'#build_enumerator' returned nil, skipping the job.")
103
138
  return
@@ -112,7 +147,7 @@ module Sidekiq
112
147
  end
113
148
 
114
149
  completed = catch(:abort) do
115
- iterate_with_enumerator(enumerator, arguments)
150
+ iterate_with_enumerator(enumerator, args)
116
151
  end
117
152
 
118
153
  on_stop
@@ -128,6 +163,10 @@ module Sidekiq
128
163
 
129
164
  private
130
165
 
166
+ def is_cancelled?
167
+ @_cancelled = Sidekiq.redis { |c| c.hget("it-#{jid}", "cancelled") }
168
+ end
169
+
131
170
  def fetch_previous_iteration_state
132
171
  state = Sidekiq.redis { |conn| conn.hgetall(iteration_key) }
133
172
 
@@ -144,6 +183,12 @@ module Sidekiq
144
183
  STATE_TTL = 30 * 24 * 60 * 60 # one month
145
184
 
146
185
  def iterate_with_enumerator(enumerator, arguments)
186
+ if is_cancelled?
187
+ logger.info { "Job cancelled" }
188
+ return true
189
+ end
190
+
191
+ time_limit = Sidekiq.default_configuration[:timeout]
147
192
  found_record = false
148
193
  state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
149
194
 
@@ -153,14 +198,21 @@ module Sidekiq
153
198
 
154
199
  is_interrupted = interrupted?
155
200
  if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
156
- flush_state
201
+ _, _, cancelled = flush_state
157
202
  state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
203
+ if cancelled == 1
204
+ @_cancelled = true
205
+ logger.info { "Job cancelled" }
206
+ return true
207
+ end
158
208
  end
159
209
 
160
210
  return false if is_interrupted
161
211
 
162
- around_iteration do
163
- each_iteration(object, *arguments)
212
+ verify_iteration_time(time_limit, object) do
213
+ around_iteration do
214
+ each_iteration(object, *arguments)
215
+ end
164
216
  end
165
217
  end
166
218
 
@@ -170,6 +222,16 @@ module Sidekiq
170
222
  @_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
171
223
  end
172
224
 
225
+ def verify_iteration_time(time_limit, object)
226
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
227
+ yield
228
+ finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
229
+ total = finish - start
230
+ if total > time_limit
231
+ logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d) when processing `%s`. This can lead to job processing problems during deploys" % [total, time_limit, object] }
232
+ end
233
+ end
234
+
173
235
  def reenqueue_iteration_job
174
236
  flush_state
175
237
  logger.debug { "Interrupting job (cursor=#{@_cursor.inspect})" }
@@ -204,6 +266,7 @@ module Sidekiq
204
266
  conn.multi do |pipe|
205
267
  pipe.hset(key, state)
206
268
  pipe.expire(key, STATE_TTL)
269
+ pipe.hget(key, "cancelled")
207
270
  end
208
271
  end
209
272
  end
@@ -5,31 +5,21 @@ module Sidekiq
5
5
  def initialize(config)
6
6
  @config = config
7
7
  @logger = @config.logger
8
- end
9
-
10
- # If true we won't do any job logging out of the box.
11
- # The user is responsible for any logging.
12
- def skip_default_logging?
13
- @config[:skip_default_job_logging]
8
+ @skip = !!@config[:skip_default_job_logging]
14
9
  end
15
10
 
16
11
  def call(item, queue)
17
- return yield if skip_default_logging?
18
-
19
- begin
20
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
21
- @logger.info("start")
12
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
13
+ @logger.info { "start" } unless @skip
22
14
 
23
- yield
24
-
25
- Sidekiq::Context.add(:elapsed, elapsed(start))
26
- @logger.info("done")
27
- rescue Exception
28
- Sidekiq::Context.add(:elapsed, elapsed(start))
29
- @logger.info("fail")
15
+ yield
30
16
 
31
- raise
32
- end
17
+ Sidekiq::Context.add(:elapsed, elapsed(start))
18
+ @logger.info { "done" } unless @skip
19
+ rescue Exception
20
+ Sidekiq::Context.add(:elapsed, elapsed(start))
21
+ @logger.info { "fail" } unless @skip
22
+ raise
33
23
  end
34
24
 
35
25
  def prepare(job_hash, &block)
@@ -36,8 +36,8 @@ module Sidekiq
36
36
  # has a heartbeat thread, caller can use `async_beat: false`
37
37
  # and instead have thread call Launcher#heartbeat every N seconds.
38
38
  def run(async_beat: true)
39
- Sidekiq.freeze!
40
39
  logger.debug { @config.merge!({}) }
40
+ Sidekiq.freeze!
41
41
  @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
42
42
  @poller.start
43
43
  @managers.each(&:start)
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent"
4
-
5
3
  module Sidekiq
6
4
  module Metrics
7
- # This is the only dependency on concurrent-ruby in Sidekiq but it's
8
- # mandatory for thread-safety until MRI supports atomic operations on values.
9
- Counter = ::Concurrent::AtomicFixnum
5
+ class Counter
6
+ def initialize
7
+ @value = 0
8
+ @lock = Mutex.new
9
+ end
10
+
11
+ def increment
12
+ @lock.synchronize { @value += 1 }
13
+ end
14
+
15
+ def value
16
+ @lock.synchronize { @value }
17
+ end
18
+ end
10
19
 
11
20
  # Implements space-efficient but statistically useful histogram storage.
12
21
  # A precise time histogram stores every time. Instead we break times into a set of
@@ -33,11 +33,26 @@ module Sidekiq
33
33
  attrs = strklass.constantize.attributes
34
34
  # Retries can push the job N times, we don't
35
35
  # want retries to reset cattr. #5692, #5090
36
- job[key] = attrs if attrs.any?
36
+ if attrs.any?
37
+ # Older rails has a bug that `CurrentAttributes#attributes` always returns
38
+ # the same hash instance. We need to dup it to avoid being accidentally mutated.
39
+ job[key] = if returns_same_object?
40
+ attrs.dup
41
+ else
42
+ attrs
43
+ end
44
+ end
37
45
  end
38
46
  end
39
47
  yield
40
48
  end
49
+
50
+ private
51
+
52
+ def returns_same_object?
53
+ ActiveSupport::VERSION::MAJOR < 8 ||
54
+ (ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
55
+ end
41
56
  end
42
57
 
43
58
  class Load
@@ -88,7 +103,7 @@ module Sidekiq
88
103
  cattrs = build_cattrs_hash(klass_or_array)
89
104
 
90
105
  config.client_middleware.add Save, cattrs
91
- config.server_middleware.add Load, cattrs
106
+ config.server_middleware.prepend Load, cattrs
92
107
  end
93
108
 
94
109
  private
@@ -138,11 +138,11 @@ module Sidekiq
138
138
  # Effectively this block denotes a "unit of work" to Rails.
139
139
  @reloader.call do
140
140
  klass = Object.const_get(job_hash["class"])
141
- inst = klass.new
142
- inst.jid = job_hash["jid"]
143
- inst._context = self
144
- @retrier.local(inst, jobstr, queue) do
145
- yield inst
141
+ instance = klass.new
142
+ instance.jid = job_hash["jid"]
143
+ instance._context = self
144
+ @retrier.local(instance, jobstr, queue) do
145
+ yield instance
146
146
  end
147
147
  end
148
148
  end
@@ -180,9 +180,9 @@ module Sidekiq
180
180
  ack = false
181
181
  Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
182
182
  Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
183
- dispatch(job_hash, queue, jobstr) do |inst|
184
- config.server_middleware.invoke(inst, job_hash, queue) do
185
- execute_job(inst, job_hash["args"])
183
+ dispatch(job_hash, queue, jobstr) do |instance|
184
+ config.server_middleware.invoke(instance, job_hash, queue) do
185
+ execute_job(instance, job_hash["args"])
186
186
  end
187
187
  end
188
188
  ack = true
@@ -216,8 +216,8 @@ module Sidekiq
216
216
  end
217
217
  end
218
218
 
219
- def execute_job(inst, cloned_args)
220
- inst.perform(*cloned_args)
219
+ def execute_job(instance, cloned_args)
220
+ instance.perform(*cloned_args)
221
221
  end
222
222
 
223
223
  # Ruby doesn't provide atomic counters out of the box so we'll
data/lib/sidekiq/rails.rb CHANGED
@@ -39,6 +39,8 @@ module Sidekiq
39
39
  # end
40
40
  initializer "sidekiq.active_job_integration" do
41
41
  ActiveSupport.on_load(:active_job) do
42
+ require "active_job/queue_adapters/sidekiq_adapter"
43
+
42
44
  include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
43
45
  end
44
46
  end
@@ -283,11 +283,11 @@ module Sidekiq
283
283
  end
284
284
 
285
285
  def process_job(job)
286
- inst = new
287
- inst.jid = job["jid"]
288
- inst.bid = job["bid"] if inst.respond_to?(:bid=)
289
- Sidekiq::Testing.server_middleware.invoke(inst, job, job["queue"]) do
290
- execute_job(inst, job["args"])
286
+ instance = new
287
+ instance.jid = job["jid"]
288
+ instance.bid = job["bid"] if instance.respond_to?(:bid=)
289
+ Sidekiq::Testing.server_middleware.invoke(instance, job, job["queue"]) do
290
+ execute_job(instance, job["args"])
291
291
  end
292
292
  end
293
293
 
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.3.2"
4
+ VERSION = "7.3.5"
5
5
  MAJOR = 7
6
+
7
+ def self.gem_version
8
+ Gem::Version.new(VERSION)
9
+ end
6
10
  end
@@ -48,8 +48,12 @@ module Sidekiq
48
48
  if content.is_a? Symbol
49
49
  unless respond_to?(:"_erb_#{content}")
50
50
  views = options[:views] || Web.settings.views
51
- src = ERB.new(File.read("#{views}/#{content}.erb")).src
52
- WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
51
+ filename = "#{views}/#{content}.erb"
52
+ src = ERB.new(File.read(filename)).src
53
+
54
+ # Need to use lineno less by 1 because erb generates a
55
+ # comment before the source code.
56
+ WebAction.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
53
57
  def _erb_#{content}
54
58
  #{src}
55
59
  end
@@ -331,12 +331,10 @@ module Sidekiq
331
331
  ########
332
332
  # Filtering
333
333
 
334
- get "/filter/metrics" do
335
- redirect "#{root_path}metrics"
336
- end
337
-
338
- post "/filter/metrics" do
334
+ route :get, :post, "/filter/metrics" do
339
335
  x = params[:substr]
336
+ return redirect "#{root_path}metrics" unless x && x != ""
337
+
340
338
  q = Sidekiq::Metrics::Query.new
341
339
  @period = h((params[:period] || "")[0..1])
342
340
  @periods = METRICS_PERIODS
@@ -346,15 +344,7 @@ module Sidekiq
346
344
  erb :metrics
347
345
  end
348
346
 
349
- get "/filter/retries" do
350
- x = params[:substr]
351
- return redirect "#{root_path}retries" unless x && x != ""
352
-
353
- @retries = search(Sidekiq::RetrySet.new, params[:substr])
354
- erb :retries
355
- end
356
-
357
- post "/filter/retries" do
347
+ route :get, :post, "/filter/retries" do
358
348
  x = params[:substr]
359
349
  return redirect "#{root_path}retries" unless x && x != ""
360
350
 
@@ -362,15 +352,7 @@ module Sidekiq
362
352
  erb :retries
363
353
  end
364
354
 
365
- get "/filter/scheduled" do
366
- x = params[:substr]
367
- return redirect "#{root_path}scheduled" unless x && x != ""
368
-
369
- @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
370
- erb :scheduled
371
- end
372
-
373
- post "/filter/scheduled" do
355
+ route :get, :post, "/filter/scheduled" do
374
356
  x = params[:substr]
375
357
  return redirect "#{root_path}scheduled" unless x && x != ""
376
358
 
@@ -378,15 +360,7 @@ module Sidekiq
378
360
  erb :scheduled
379
361
  end
380
362
 
381
- get "/filter/dead" do
382
- x = params[:substr]
383
- return redirect "#{root_path}morgue" unless x && x != ""
384
-
385
- @dead = search(Sidekiq::DeadSet.new, params[:substr])
386
- erb :morgue
387
- end
388
-
389
- post "/filter/dead" do
363
+ route :get, :post, "/filter/dead" do
390
364
  x = params[:substr]
391
365
  return redirect "#{root_path}morgue" unless x && x != ""
392
366
 
@@ -39,10 +39,13 @@ module Sidekiq
39
39
  route(DELETE, path, &block)
40
40
  end
41
41
 
42
- def route(method, path, &block)
42
+ def route(*methods, path, &block)
43
43
  @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
44
44
 
45
- @routes[method] << WebRoute.new(method, path, block)
45
+ methods.each do |method|
46
+ method = method.to_s.upcase
47
+ @routes[method] << WebRoute.new(method, path, block)
48
+ end
46
49
  end
47
50
 
48
51
  def match(env)
data/lib/sidekiq/web.rb CHANGED
@@ -213,7 +213,7 @@ module Sidekiq
213
213
  Sidekiq::WebApplication.helpers WebHelpers
214
214
  Sidekiq::WebApplication.helpers Sidekiq::Paginator
215
215
 
216
- Sidekiq::WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
216
+ Sidekiq::WebAction.class_eval <<-RUBY, Web::LAYOUT, -1 # standard:disable Style/EvalWithLocation
217
217
  def _render
218
218
  #{ERB.new(File.read(Web::LAYOUT)).src}
219
219
  end
data/lib/sidekiq.rb CHANGED
@@ -102,18 +102,19 @@ module Sidekiq
102
102
  def self.freeze!
103
103
  @frozen = true
104
104
  @config_blocks = nil
105
+ default_configuration.freeze!
105
106
  end
106
107
 
107
108
  # Creates a Sidekiq::Config instance that is more tuned for embedding
108
109
  # within an arbitrary Ruby process. Notably it reduces concurrency by
109
110
  # default so there is less contention for CPU time with other threads.
110
111
  #
111
- # inst = Sidekiq.configure_embed do |config|
112
+ # instance = Sidekiq.configure_embed do |config|
112
113
  # config.queues = %w[critical default low]
113
114
  # end
114
- # inst.run
115
+ # instance.run
115
116
  # sleep 10
116
- # inst.stop
117
+ # instance.stop
117
118
  #
118
119
  # NB: it is really easy to overload a Ruby process with threads due to the GIL.
119
120
  # I do not recommend setting concurrency higher than 2-3.
data/sidekiq.gemspec CHANGED
@@ -26,6 +26,5 @@ Gem::Specification.new do |gem|
26
26
  gem.add_dependency "redis-client", ">= 0.22.2"
27
27
  gem.add_dependency "connection_pool", ">= 2.3.0"
28
28
  gem.add_dependency "rack", ">= 2.2.4"
29
- gem.add_dependency "concurrent-ruby", "< 2"
30
29
  gem.add_dependency "logger"
31
30
  end
@@ -650,6 +650,11 @@ div.interval-slider input {
650
650
  .redis-url {
651
651
  max-width: 160px;
652
652
  }
653
+
654
+ .navbar-fixed-bottom .nav {
655
+ margin-left: -15px;
656
+ margin-right: -15px;
657
+ }
653
658
  }
654
659
 
655
660
  @media (min-width: 992px) {
data/web/locales/en.yml CHANGED
@@ -95,7 +95,6 @@ en:
95
95
  TotalExecutionTime: Total Execution Time
96
96
  AvgExecutionTime: Average Execution Time
97
97
  Context: Context
98
- Bucket: Bucket
99
98
  NoJobMetricsFound: No recent job metrics were found
100
99
  Filter: Filter
101
100
  AnyJobContent: Any job content
data/web/locales/fr.yml CHANGED
@@ -95,5 +95,4 @@ fr:
95
95
  TotalExecutionTime: Temps d'exécution total
96
96
  AvgExecutionTime: Temps d'exécution moyen
97
97
  Context: Contexte
98
- Bucket: Bucket
99
98
  NoJobMetricsFound: Aucune statistique de tâche récente n'a été trouvée
data/web/locales/gd.yml CHANGED
@@ -95,5 +95,4 @@ gd:
95
95
  TotalExecutionTime: Ùine iomlan nan gnìomhan
96
96
  AvgExecutionTime: Ùine cuibheasach nan gnìomhan
97
97
  Context: Co-theacsa
98
- Bucket: Bucaid
99
98
  NoJobMetricsFound: Cha deach meatraigeachd o chionn goirid air obair a lorg
data/web/locales/it.yml CHANGED
@@ -6,44 +6,60 @@ it:
6
6
  AreYouSureDeleteJob: Sei sicuro di voler cancellare questo lavoro?
7
7
  AreYouSureDeleteQueue: Sei sicuro di voler cancellare la coda %{queue}?
8
8
  Arguments: Argomenti
9
+ BackToApp: Torna all'App
9
10
  Busy: Occupato
10
11
  Class: Classe
11
12
  Connections: Connessioni
13
+ CreatedAt: Creato il
12
14
  CurrentMessagesInQueue: Messaggi in <span class='title'>%{queue}</span>
13
15
  Dashboard: Dashboard
14
16
  Dead: Arrestato
15
17
  DeadJobs: Lavori arrestati
16
18
  Delete: Cancella
17
19
  DeleteAll: Cancella tutti
20
+ Deploy: Distribuire
18
21
  Enqueued: In coda
19
22
  Error: Errore
20
23
  ErrorBacktrace: Backtrace dell'errore
21
24
  ErrorClass: Classe dell'errore
22
25
  ErrorMessage: Messaggio di errore
26
+ ExecutionTime: Tempo di esecuzione
23
27
  Extras: Extra
24
28
  Failed: Fallito
25
29
  Failures: Fallimenti
30
+ Failure: Fallimento
26
31
  GoBack: ← Indietro
27
32
  History: Storia
28
33
  Job: Lavoro
29
34
  Jobs: Lavori
30
35
  Kill: Uccidere
36
+ KillAll: Uccidere tutti
31
37
  LastRetry: Ultimo tentativo
38
+ Latency: Latenza
32
39
  LivePoll: Live poll
33
40
  MemoryUsage: Memoria utilizzata
41
+ Name: Nome
34
42
  Namespace: Namespace
35
43
  NextRetry: Prossimo tentativo
36
44
  NoDeadJobsFound: Non ci sono lavori arrestati
37
45
  NoRetriesFound: Non sono stati trovati nuovi tentativi
38
46
  NoScheduledFound: Non ci sono lavori pianificati
47
+ NotYetEnqueued: Non ancora in coda
39
48
  OneMonth: 1 mese
40
49
  OneWeek: 1 settimana
41
50
  OriginallyFailed: Primo fallimento
51
+ Pause: Metti in pausa
52
+ Paused: In pausa
42
53
  PeakMemoryUsage: Memoria utilizzata (max.)
54
+ Plugins: Plugins
55
+ PollingInterval: Intervallo di polling
56
+ Process: Processo
43
57
  Processed: Processato
44
58
  Processes: Processi
45
59
  Queue: Coda
46
60
  Queues: Code
61
+ Quiet: Silenzia
62
+ QuietAll: Silenzia Tutti
47
63
  Realtime: Tempo reale
48
64
  Retries: Nuovi tentativi
49
65
  RetryAll: Riprova tutti
@@ -51,19 +67,34 @@ it:
51
67
  RetryNow: Riprova
52
68
  Scheduled: Pianificato
53
69
  ScheduledJobs: Lavori pianificati
70
+ Seconds: Secondi
54
71
  ShowAll: Mostra tutti
55
72
  SixMonths: 6 mesi
56
73
  Size: Dimensione
57
74
  Started: Iniziato
58
75
  Status: Stato
76
+ Stop: Ferma
77
+ StopAll: Ferma Tutti
59
78
  StopPolling: Ferma il polling
79
+ Success: Successo
80
+ Summary: Riepilogo
60
81
  Thread: Thread
61
- Threads: Thread
82
+ Threads: Threads
62
83
  ThreeMonths: 3 mesi
63
84
  Time: Ora
85
+ Unpause: Riattiva
64
86
  Uptime: Uptime (giorni)
87
+ Utilization: Utilizzo
65
88
  Version: Versione
66
89
  When: Quando
67
90
  Worker: Lavoratore
68
91
  active: attivo
69
92
  idle: inattivo
93
+ Metrics: Metriche
94
+ NoDataFound: Nessun dato trovato
95
+ TotalExecutionTime: Tempo totale di esecuzione
96
+ AvgExecutionTime: Tempo medio di esecuzione
97
+ Context: Contesto
98
+ NoJobMetricsFound: Metriche recenti di lavoro non trovate
99
+ Filter: Filtro
100
+ AnyJobContent: Qualsiasi contenuto di lavoro
data/web/locales/ja.yml CHANGED
@@ -87,5 +87,4 @@ ja:
87
87
  TotalExecutionTime: 合計実行時間
88
88
  AvgExecutionTime: 平均実行時間
89
89
  Context: コンテキスト
90
- Bucket: バケット
91
90
  NoJobMetricsFound: 直近のジョブメトリクスが見つかりませんでした
@@ -7,7 +7,6 @@
7
7
  Arguments: Argumentos
8
8
  AvgExecutionTime: Tempo médio de execução
9
9
  BackToApp: De volta ao aplicativo
10
- Bucket: Bucket
11
10
  Busy: Ocupados
12
11
  Class: Classe
13
12
  Connections: Conexões
@@ -93,4 +92,4 @@
93
92
  Utilization: Utilização
94
93
  Version: Versão
95
94
  When: Quando
96
- Worker: Trabalhador
95
+ Worker: Trabalhador
data/web/locales/tr.yml CHANGED
@@ -95,7 +95,6 @@ tr:
95
95
  TotalExecutionTime: Toplam Yürütme Süresi
96
96
  AvgExecutionTime: Ortalama Yürütme Süresi
97
97
  Context: Bağlam
98
- Bucket: Kova
99
98
  NoJobMetricsFound: Son iş metrikleri bulunamadı
100
99
  Filter: Filtre
101
100
  AnyJobContent: Herhangi bir iş içeriği
data/web/locales/uk.yml CHANGED
@@ -6,31 +6,39 @@ uk:
6
6
  AreYouSureDeleteJob: Ви впевнені у тому, що хочете видалити задачу?
7
7
  AreYouSureDeleteQueue: Ви впевнені у тому, що хочете видалити чергу %{queue}?
8
8
  Arguments: Аргументи
9
+ BackToApp: Назад
9
10
  Busy: Зайнятих
10
11
  Class: Клас
11
12
  Connections: З'єднань
13
+ CreatedAt: Створено
12
14
  CurrentMessagesInQueue: Поточні задачі у черзі <span class='title'>%{queue}</span>
13
15
  Dashboard: Панель керування
14
16
  Dead: Вбитих
15
17
  DeadJobs: Вбиті задачі
16
18
  Delete: Видалити
17
19
  DeleteAll: Видалити усі
20
+ Deploy: Деплой
18
21
  Enqueued: У черзі
19
22
  Error: Помилка
20
23
  ErrorBacktrace: Трасування помилки
21
24
  ErrorClass: Клас помилки
22
25
  ErrorMessage: Повідомлення про помилку
26
+ ExecutionTime: Час виконання
23
27
  Extras: Додатково
24
28
  Failed: Невдалих
25
29
  Failures: Невдачі
30
+ Failure: Невдача
26
31
  GoBack: ← Назад
27
32
  History: Історія
28
33
  Job: Задача
29
34
  Jobs: Задачі
30
- Kill: Вбиваємо
35
+ Kill: Вбити
36
+ KillAll: Вбити все
31
37
  LastRetry: Остання спроба
38
+ Latency: Затримка
32
39
  LivePoll: Постійне опитування
33
40
  MemoryUsage: Використання пам'яті
41
+ Name: Назва
34
42
  Namespace: Простір імен
35
43
  NextRetry: Наступна спроба
36
44
  NoDeadJobsFound: Вбитих задач не знайдено
@@ -40,10 +48,12 @@ uk:
40
48
  OneMonth: 1 місяць
41
49
  OneWeek: 1 тиждень
42
50
  OriginallyFailed: Перша невдала спроба
51
+ Pause: Призупинити
43
52
  Paused: Призупинено
44
53
  PeakMemoryUsage: Максимальне використання пам'яті
45
54
  Plugins: Плагіни
46
55
  PollingInterval: Інтервал опитування
56
+ Process: Процес
47
57
  Processed: Опрацьовано
48
58
  Processes: Процеси
49
59
  Queue: Черга
@@ -57,6 +67,7 @@ uk:
57
67
  RetryNow: Повторити зараз
58
68
  Scheduled: Заплановано
59
69
  ScheduledJobs: Заплановані задачі
70
+ Seconds: Секунди
60
71
  ShowAll: Відобразити усі
61
72
  SixMonths: 6 місяців
62
73
  Size: Розмір
@@ -65,13 +76,25 @@ uk:
65
76
  Stop: Зупинити
66
77
  StopAll: Зупинити усі
67
78
  StopPolling: Зупинити опитування
79
+ Success: Успіх
80
+ Summary: Підсумок
68
81
  Thread: Потік
69
82
  Threads: Потоки
70
83
  ThreeMonths: 3 місяці
71
84
  Time: Час
85
+ Unpause: Відновити
72
86
  Uptime: Днів безперебійної роботи
87
+ Utilization: Утилізація
73
88
  Version: Версія
74
89
  When: Коли
75
90
  Worker: Обробник
76
91
  active: активний
77
92
  idle: незайнятий
93
+ Metrics: Метрики
94
+ NoDataFound: Даних не знайдено
95
+ TotalExecutionTime: Загальний час виконання
96
+ AvgExecutionTime: Середній час виконання
97
+ Context: Контекст
98
+ NoJobMetricsFound: Недавніх метрик задачі не знайдено
99
+ Filter: Фільтр
100
+ AnyJobContent: Будь-який атрибут задачі
@@ -89,7 +89,6 @@ zh-cn: # <---- change this to your locale code
89
89
  TotalExecutionTime: 总执行时间
90
90
  AvgExecutionTime: 平均执行时间
91
91
  Context: 上下文
92
- Bucket: 桶
93
92
  NoJobMetricsFound: 无任务相关指标数据
94
93
  Success: 成功
95
94
  Failure: 失败
@@ -98,5 +98,4 @@ zh-tw: # <---- change this to your locale code
98
98
  TotalExecutionTime: 總執行時間
99
99
  AvgExecutionTime: 平均執行時間
100
100
  Context: 上下文
101
- Bucket: 桶
102
101
  NoJobMetricsFound: 找無工作相關計量資料
@@ -54,8 +54,8 @@
54
54
  <th><%= t('Name') %></th>
55
55
  <th><%= t('Success') %></th>
56
56
  <th><%= t('Failure') %></th>
57
- <th><%= t('TotalExecutionTime') %> (Seconds)</th>
58
- <th><%= t('AvgExecutionTime') %> (Seconds)</th>
57
+ <th><%= t('TotalExecutionTime') %> (<%= t('Seconds') %>)</th>
58
+ <th><%= t('AvgExecutionTime') %> (<%= t('Seconds') %>)</th>
59
59
  </tr>
60
60
  <% if job_results.any? %>
61
61
  <% job_results.each_with_index do |(kls, jr), i| %>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.3.2
4
+ version: 7.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-04 00:00:00.000000000 Z
11
+ date: 2024-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 2.2.4
55
- - !ruby/object:Gem::Dependency
56
- name: concurrent-ruby
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "<"
60
- - !ruby/object:Gem::Version
61
- version: '2'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "<"
67
- - !ruby/object:Gem::Version
68
- version: '2'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: logger
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -96,6 +82,7 @@ files:
96
82
  - bin/sidekiq
97
83
  - bin/sidekiqload
98
84
  - bin/sidekiqmon
85
+ - lib/active_job/queue_adapters/sidekiq_adapter.rb
99
86
  - lib/generators/sidekiq/job_generator.rb
100
87
  - lib/generators/sidekiq/templates/job.rb.erb
101
88
  - lib/generators/sidekiq/templates/job_spec.rb.erb
@@ -245,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
245
232
  - !ruby/object:Gem::Version
246
233
  version: '0'
247
234
  requirements: []
248
- rubygems_version: 3.5.11
235
+ rubygems_version: 3.5.16
249
236
  signing_key:
250
237
  specification_version: 4
251
238
  summary: Simple, efficient background processing for Ruby