sidekiq 7.3.9 → 8.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +21 -4
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +10 -10
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +5 -5
  7. data/lib/sidekiq/api.rb +108 -36
  8. data/lib/sidekiq/capsule.rb +1 -1
  9. data/lib/sidekiq/cli.rb +15 -19
  10. data/lib/sidekiq/client.rb +13 -16
  11. data/lib/sidekiq/component.rb +30 -2
  12. data/lib/sidekiq/config.rb +18 -15
  13. data/lib/sidekiq/embedded.rb +1 -0
  14. data/lib/sidekiq/job/iterable.rb +2 -4
  15. data/lib/sidekiq/job_retry.rb +2 -2
  16. data/lib/sidekiq/job_util.rb +5 -1
  17. data/lib/sidekiq/launcher.rb +1 -1
  18. data/lib/sidekiq/logger.rb +6 -10
  19. data/lib/sidekiq/manager.rb +0 -1
  20. data/lib/sidekiq/metrics/query.rb +1 -3
  21. data/lib/sidekiq/middleware/current_attributes.rb +5 -17
  22. data/lib/sidekiq/paginator.rb +8 -1
  23. data/lib/sidekiq/processor.rb +21 -14
  24. data/lib/sidekiq/profiler.rb +59 -0
  25. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  26. data/lib/sidekiq/testing.rb +2 -2
  27. data/lib/sidekiq/version.rb +2 -2
  28. data/lib/sidekiq/web/action.rb +101 -85
  29. data/lib/sidekiq/web/application.rb +339 -333
  30. data/lib/sidekiq/web/config.rb +116 -0
  31. data/lib/sidekiq/web/helpers.rb +40 -15
  32. data/lib/sidekiq/web/router.rb +60 -76
  33. data/lib/sidekiq/web.rb +51 -156
  34. data/sidekiq.gemspec +5 -4
  35. data/web/assets/javascripts/application.js +6 -13
  36. data/web/assets/javascripts/base-charts.js +30 -18
  37. data/web/assets/javascripts/metrics.js +1 -1
  38. data/web/assets/stylesheets/style.css +750 -0
  39. data/web/locales/ar.yml +1 -0
  40. data/web/locales/cs.yml +1 -0
  41. data/web/locales/da.yml +1 -0
  42. data/web/locales/de.yml +1 -0
  43. data/web/locales/el.yml +1 -0
  44. data/web/locales/en.yml +6 -0
  45. data/web/locales/es.yml +24 -2
  46. data/web/locales/fa.yml +1 -0
  47. data/web/locales/fr.yml +1 -0
  48. data/web/locales/gd.yml +1 -0
  49. data/web/locales/he.yml +1 -0
  50. data/web/locales/hi.yml +1 -0
  51. data/web/locales/it.yml +1 -0
  52. data/web/locales/ja.yml +1 -0
  53. data/web/locales/ko.yml +1 -0
  54. data/web/locales/lt.yml +1 -0
  55. data/web/locales/nb.yml +1 -0
  56. data/web/locales/nl.yml +1 -0
  57. data/web/locales/pl.yml +1 -0
  58. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  59. data/web/locales/pt.yml +1 -0
  60. data/web/locales/ru.yml +1 -0
  61. data/web/locales/sv.yml +1 -0
  62. data/web/locales/ta.yml +1 -0
  63. data/web/locales/tr.yml +1 -0
  64. data/web/locales/uk.yml +1 -0
  65. data/web/locales/ur.yml +1 -0
  66. data/web/locales/vi.yml +1 -0
  67. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  68. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  69. data/web/views/_footer.erb +31 -33
  70. data/web/views/_job_info.erb +91 -89
  71. data/web/views/_metrics_period_select.erb +1 -1
  72. data/web/views/_nav.erb +14 -21
  73. data/web/views/_paging.erb +23 -21
  74. data/web/views/_poll_link.erb +2 -2
  75. data/web/views/_summary.erb +16 -16
  76. data/web/views/busy.erb +124 -122
  77. data/web/views/dashboard.erb +61 -66
  78. data/web/views/dead.erb +31 -27
  79. data/web/views/filtering.erb +3 -3
  80. data/web/views/layout.erb +5 -21
  81. data/web/views/metrics.erb +83 -80
  82. data/web/views/metrics_for_job.erb +39 -42
  83. data/web/views/morgue.erb +61 -70
  84. data/web/views/profiles.erb +43 -0
  85. data/web/views/queue.erb +54 -52
  86. data/web/views/queues.erb +43 -41
  87. data/web/views/retries.erb +66 -75
  88. data/web/views/retry.erb +32 -27
  89. data/web/views/scheduled.erb +58 -54
  90. data/web/views/scheduled_job_info.erb +1 -1
  91. metadata +31 -18
  92. data/web/assets/stylesheets/application-dark.css +0 -147
  93. data/web/assets/stylesheets/application-rtl.css +0 -163
  94. data/web/assets/stylesheets/application.css +0 -759
  95. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  96. data/web/assets/stylesheets/bootstrap.css +0 -5
  97. data/web/views/_status.erb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dccc402e8f4e309a47a2adb62418fa29989012e8c94f34aa11200b098dc375f2
4
- data.tar.gz: 71b64a582d5971b045d60bae38dc26da265b60ff9a099ca3b2f9c7df9035e805
3
+ metadata.gz: f7280923fe38a56b42bb33c639f9e0a365c1f585f928e8cdfd3ce06fe989fc7e
4
+ data.tar.gz: e8c6a01e393390f1a3ab3a48c04b3621efd5621c6b184240c3e8316602e24e9c
5
5
  SHA512:
6
- metadata.gz: d9148b613a222ca9617ceebe25bb04a5d086edacdafad4a426b6232662d3b583db462dc5e0491297f7859037c5166bcb541bddcd3949f6d3b5a1c2e3fc572b65
7
- data.tar.gz: 93a797cefd1a68adb236c7538c28571467c328d6965af0bc5ce505a1391d6f5b6e2ae2c87b42110e837dcfa8d788aff55309806bbb1bcd181e205c3bc66adc29
6
+ metadata.gz: 076a8f70170dfdc6ec78430ca41b182ed986453bdfbcfe24b7dd5ef1d8f2321b560cb94201c2f2c3a30c2ce42a31e38ce14151f0ebbee90bc97a2463b427747a
7
+ data.tar.gz: 8cc4828931a73da67ae40edb14508b82624781b9732280ce9e8db615c8bed1039b89b8b50db54c35bbffd4a010f904598be2bb4aa037228bbd4b4b325b2b9421
data/Changes.md CHANGED
@@ -2,12 +2,29 @@
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.9
5
+ HEAD / main
6
6
  ----------
7
7
 
8
- - Only require activejob if necessary [#6584]
9
- - Fix iterable job cancellation [#6589]
10
- - Web UI accessibility improvements [#6604]
8
+ - **WARNING** The underlying class name for Active Jobs has changed from `ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper` to `Sidekiq::ActiveJob::Wrapper`.
9
+ - **WARNING** The `created_at` and `enqueued_at` attributes are now stored as
10
+ integer milliseconds, rather than epoch floats. This is meant to avoid precision
11
+ issues with JSON and JavaScript's 53-bit Floats. Example:
12
+ `"created_at" => 1234567890.123456` -> `"created_at" => 1234567890123`.
13
+ - **NEW FEATURE** Job Profiling is now supported with [Vernier](https://vernier.prof)
14
+ which makes it really easy to performance tune your slow jobs.
15
+ The Web UI contains a new **Profiles** tab to view any collected profile data.
16
+ Please read the new [Profiling](https://github.com/sidekiq/sidekiq/wiki/Profiling) wiki page for details.
17
+ - CurrentAttribute support now uses `ActiveJob::Arguments` to serialize the context object, supporting Symbols and GlobalID.
18
+ The change should be backwards compatible. [#6510]
19
+ - Freshen up `Sidekiq::Web` to simplify the code and improve security [#6532]
20
+ The CSS has been rewritten from scratch to remove the Bootstrap framework.
21
+ - Default error logging has been modified to use Ruby's `Exception#detailed_message` and `#full_message` APIs.
22
+ - CI now runs against Redis, Dragonfly and Valkey.
23
+ - The Web UI's language picker now shows options in the native language
24
+ - Remove global variable usage within the codebase
25
+ - Adjust Sidekiq's default thread priority to -1 for a 50ms timeslice.
26
+ This can help avoid TimeoutErrors when Sidekiq is overloaded. [#6543]
27
+ - Support: Redis 7.2+, Ruby 3.2+, Rails 7.0+
11
28
 
12
29
  7.3.8
13
30
  ----------
data/README.md CHANGED
@@ -4,21 +4,23 @@ Sidekiq
4
4
  [![Gem Version](https://badge.fury.io/rb/sidekiq.svg)](https://rubygems.org/gems/sidekiq)
5
5
  ![Build](https://github.com/sidekiq/sidekiq/workflows/CI/badge.svg)
6
6
 
7
- Simple, efficient background processing for Ruby.
7
+ Simple, efficient background jobs for Ruby.
8
8
 
9
9
  Sidekiq uses threads to handle many jobs at the same time in the
10
- same process. It does not require Rails but will integrate tightly with
11
- Rails to make background processing dead simple.
10
+ same process. Sidekiq can be used by any Ruby application.
12
11
 
13
12
 
14
13
  Requirements
15
14
  -----------------
16
15
 
17
- - Redis: Redis 6.2+ or Dragonfly 1.13+
18
- - Ruby: MRI 2.7+ or JRuby 9.3+.
16
+ - Redis: Redis 7.2+, Valkey 7.2+ or Dragonfly 1.13+
17
+ - Ruby: MRI 3.2+ or JRuby 9.4+.
19
18
 
20
- Sidekiq 7.0 supports Rails 6.0+ but does not require it.
21
- As of 7.2, Sidekiq supports Dragonfly as an alternative to Redis for data storage.
19
+ Sidekiq 8.0 supports Rails and Active Job 7.0+.
20
+
21
+ Sidekiq supports [Valkey](https://valkey.io) and [Dragonfly](https://www.dragonflydb.io) as Redis alternatives.
22
+ Redis 7.2.4 is considered to be the canonical implementation.
23
+ Incompatibilities with that version are considered bugs.
22
24
 
23
25
  Installation
24
26
  -----------------
@@ -42,6 +44,7 @@ The benchmark in `bin/sidekiqload` creates 500,000 no-op jobs and drains them as
42
44
  This requires a lot of Redis network I/O and JSON parsing.
43
45
  This benchmark is IO-bound so we increase the concurrency to 25.
44
46
  If your application is sending lots of emails or performing other network-intensive work, you could see a similar benefit but be careful not to saturate the CPU.
47
+ Real world applications will rarely if ever need to use concurrency greater than 10.
45
48
 
46
49
  Version | Time to process 500k jobs | Throughput (jobs/sec) | Ruby | Concurrency | Job Type
47
50
  -----------------|------|---------|---------|------------------------|---
@@ -62,7 +65,7 @@ Want to Upgrade?
62
65
  Use `bundle up sidekiq` to upgrade Sidekiq and all its dependencies.
63
66
  Upgrade notes between each major version can be found in the `docs/` directory.
64
67
 
65
- I also sell Sidekiq Pro and Sidekiq Enterprise, extensions to Sidekiq which provide more
68
+ I also sell [Sidekiq Pro](https://billing.contribsys.com/spro/) and [Sidekiq Enterprise](https://billing.contribsys.com/sent/new.cgi), extensions to Sidekiq which provide more
66
69
  features, a commercial-friendly license and allow you to support high
67
70
  quality open source development all at the same time. Please see the
68
71
  [Sidekiq](https://sidekiq.org/) homepage for more detail.
@@ -71,7 +74,7 @@ quality open source development all at the same time. Please see the
71
74
  Problems?
72
75
  -----------------
73
76
 
74
- **Please do not directly email any Sidekiq committers with questions or problems.**
77
+ **Do not directly email any Sidekiq committers with questions or problems.**
75
78
  A community is best served when discussions are held in public.
76
79
 
77
80
  If you have a problem, please review the [FAQ](https://github.com/sidekiq/sidekiq/wiki/FAQ) and [Troubleshooting](https://github.com/sidekiq/sidekiq/wiki/Problems-and-Troubleshooting) wiki pages.
@@ -86,21 +89,21 @@ Useful resources:
86
89
  * Occasional announcements are made to the [@sidekiq](https://ruby.social/@sidekiq) Mastodon account.
87
90
  * The [Sidekiq tag](https://stackoverflow.com/questions/tagged/sidekiq) on Stack Overflow has lots of useful Q & A.
88
91
 
89
- Every Thursday morning is Sidekiq office hour: I video chat and answer questions.
92
+ Every Thursday morning is Sidekiq Office Hour: I video chat and answer questions.
90
93
  See the [Sidekiq support page](https://sidekiq.org/support.html) for details.
91
94
 
92
95
  Contributing
93
96
  -----------------
94
97
 
95
- Please see [the contributing guidelines](https://github.com/sidekiq/sidekiq/blob/main/.github/contributing.md).
98
+ See [the contributing guidelines](https://github.com/sidekiq/sidekiq/blob/main/.github/contributing.md).
96
99
 
97
100
  License
98
101
  -----------------
99
102
 
100
- Please see [LICENSE.txt](https://github.com/sidekiq/sidekiq/blob/main/LICENSE.txt) for licensing details.
103
+ See [LICENSE.txt](https://github.com/sidekiq/sidekiq/blob/main/LICENSE.txt) for licensing details.
101
104
  The license for Sidekiq Pro and Sidekiq Enterprise can be found in [COMM-LICENSE.txt](https://github.com/sidekiq/sidekiq/blob/main/COMM-LICENSE.txt).
102
105
 
103
106
  Author
104
107
  -----------------
105
108
 
106
- Mike Perham, [@getajobmike](https://ruby.social/@getajobmike) / [@sidekiq](https://ruby.social/@sidekiq), [https://www.mikeperham.com](https://www.mikeperham.com) / [https://www.contribsys.com](https://www.contribsys.com)
109
+ Mike Perham, [mastodon](https://ruby.social/@getajobmike), [https://www.mikeperham.com](https://www.mikeperham.com) / [https://www.contribsys.com](https://www.contribsys.com)
data/bin/sidekiqload CHANGED
@@ -28,7 +28,7 @@ require "ruby-prof" if ENV["PROFILE"]
28
28
  require "bundler/setup"
29
29
  Bundler.require(:default, :load_test)
30
30
 
31
- latency = Integer(ENV["LATENCY"] || 1)
31
+ latency = Integer(ENV["LATENCY"] || 0)
32
32
  if latency > 0
33
33
  # brew tap shopify/shopify
34
34
  # brew install toxiproxy
@@ -86,7 +86,7 @@ class Loader
86
86
  def initialize
87
87
  @iter = ENV["GC"] ? 10 : 500
88
88
  @count = Integer(ENV["COUNT"] || 1_000)
89
- @latency = Integer(ENV["LATENCY"] || 1)
89
+ @latency = Integer(ENV["LATENCY"] || 0)
90
90
  end
91
91
 
92
92
  def configure
@@ -137,7 +137,7 @@ class Loader
137
137
  end
138
138
 
139
139
  def setup
140
- Sidekiq.logger.error("Setup RSS: #{Process.rss}")
140
+ Sidekiq.logger.warn("Setup RSS: #{Process.rss}")
141
141
  Sidekiq.redis { |c| c.flushdb }
142
142
  start = Time.now
143
143
  if ENV["AJ"]
@@ -167,9 +167,9 @@ class Loader
167
167
  if total == 0
168
168
  ending = Time.now - @start
169
169
  size = @iter * @count
170
- Sidekiq.logger.error("Done, #{size} jobs in #{ending} sec, #{(size / ending).to_i} jobs/sec")
171
- Sidekiq.logger.error("Ending RSS: #{Process.rss}")
172
- Sidekiq.logger.error("Now here's the latency for three jobs")
170
+ Sidekiq.logger.warn("Done, #{size} jobs in #{ending} sec, #{(size / ending).to_i} jobs/sec")
171
+ Sidekiq.logger.warn("Ending RSS: #{Process.rss}")
172
+ Sidekiq.logger.warn("Now here's the latency for three jobs")
173
173
 
174
174
  if ENV["AJ"]
175
175
  LoadJob.perform_later("", 1, {}, Time.now.to_f)
@@ -191,7 +191,7 @@ class Loader
191
191
  end
192
192
 
193
193
  def with_latency(latency, &block)
194
- Sidekiq.logger.error "Simulating #{latency}ms of latency between Sidekiq and redis"
194
+ Sidekiq.logger.warn "Simulating #{latency}ms of latency between Sidekiq and redis"
195
195
  if latency > 0
196
196
  Toxiproxy[:redis].downstream(:latency, latency: latency).apply(&block)
197
197
  else
@@ -204,8 +204,8 @@ class Loader
204
204
  monitor
205
205
 
206
206
  if ENV["PROFILE"]
207
- RubyProf.exclude_threads = [@monitor]
208
- RubyProf.start
207
+ $profile = RubyProf::Profile.new(exclude_threads: [@monitor])
208
+ $profile.start
209
209
  elsif ENV["GC"]
210
210
  GC.start
211
211
  GC.compact
@@ -236,7 +236,7 @@ class Loader
236
236
  Sidekiq.logger.error("GC End RSS: #{Process.rss}") if ENV["GC"]
237
237
  if ENV["PROFILE"]
238
238
  Sidekiq.logger.error("Profiling...")
239
- result = RubyProf.stop
239
+ result = $profile.stop
240
240
  printer = RubyProf::GraphHtmlPrinter.new(result)
241
241
  printer.print(File.new("output.html", "w"), min_percent: 1)
242
242
  end
data/bin/webload ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler'
4
+ Bundler.setup
5
+
6
+ # This skeleton allows you to run Sidekiq::Web page rendering
7
+ # through Vernier for tuning.
8
+ require "sidekiq/web"
9
+ require "rack/test"
10
+ require "vernier"
11
+
12
+ Sidekiq::Web.configure do |config|
13
+ config.middlewares.clear # remove csrf
14
+ end
15
+
16
+ class SomeJob
17
+ include Sidekiq::Job
18
+ end
19
+
20
+ class BenchWeb
21
+ include Rack::Test::Methods
22
+
23
+ def app
24
+ Sidekiq::Web.new
25
+ end
26
+
27
+ def warmup(page = "/scheduled")
28
+ # Sidekiq.redis {|c| c.flushdb }
29
+
30
+ # 100.times do |idx|
31
+ # SomeJob.perform_at(idx, 1, 3, "mike", {"foo" => "bar"})
32
+ # end
33
+
34
+ 100.times do
35
+ get page
36
+ end
37
+ end
38
+
39
+ def load(page = "/scheduled", count = 10_000)
40
+ profile do
41
+ count.times do
42
+ get page
43
+ raise last_response.inspect unless last_response.status == 200
44
+ end
45
+ end
46
+ end
47
+
48
+ def profile(&)
49
+ if ENV["PROF"]
50
+ Vernier.profile(out: "profile.json.gz", &)
51
+ else
52
+ yield
53
+ end
54
+ end
55
+ end
56
+
57
+ def timer(name="block", count = 10_000)
58
+ a = Time.now
59
+ yield count
60
+ b = Time.now
61
+ puts "#{name} in #{b - a} sec"
62
+ end
63
+
64
+ page = "/busy"
65
+ b = BenchWeb.new
66
+ b.warmup(page)
67
+ timer(page) do |count|
68
+ b.load(page, count)
69
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ActiveJob
4
4
  module QueueAdapters
5
- # Explicitly remove the implementation existing in older rails'.
5
+ # Explicitly remove the implementation existing in older Rails.
6
6
  remove_const(:SidekiqAdapter) if const_defined?(:SidekiqAdapter)
7
7
 
8
8
  # Sidekiq adapter for Active Job
@@ -20,7 +20,7 @@ module ActiveJob
20
20
 
21
21
  # @api private
22
22
  def enqueue(job)
23
- job.provider_job_id = JobWrapper.set(
23
+ job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(
24
24
  wrapped: job.class,
25
25
  queue: job.queue_name
26
26
  ).perform_async(job.serialize)
@@ -28,7 +28,7 @@ module ActiveJob
28
28
 
29
29
  # @api private
30
30
  def enqueue_at(job, timestamp)
31
- job.provider_job_id = JobWrapper.set(
31
+ job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(
32
32
  wrapped: job.class,
33
33
  queue: job.queue_name
34
34
  ).perform_at(timestamp, job.serialize)
@@ -43,7 +43,7 @@ module ActiveJob
43
43
 
44
44
  if immediate_jobs.any?
45
45
  jids = Sidekiq::Client.push_bulk(
46
- "class" => JobWrapper,
46
+ "class" => Sidekiq::ActiveJob::Wrapper,
47
47
  "wrapped" => job_class,
48
48
  "queue" => queue,
49
49
  "args" => immediate_jobs.map { |job| [job.serialize] }
@@ -53,7 +53,7 @@ module ActiveJob
53
53
 
54
54
  if scheduled_jobs.any?
55
55
  jids = Sidekiq::Client.push_bulk(
56
- "class" => JobWrapper,
56
+ "class" => Sidekiq::ActiveJob::Wrapper,
57
57
  "wrapped" => job_class,
58
58
  "queue" => queue,
59
59
  "args" => scheduled_jobs.map { |job| [job.serialize] },
data/lib/sidekiq/api.rb CHANGED
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq"
4
-
5
3
  require "zlib"
6
- require "set"
7
4
 
5
+ require "sidekiq"
8
6
  require "sidekiq/metrics/query"
9
7
 
10
8
  #
@@ -101,11 +99,22 @@ module Sidekiq
101
99
  rescue
102
100
  {}
103
101
  end
104
- now = Time.now.to_f
105
- thence = job["enqueued_at"] || now
106
- now - thence
102
+
103
+ enqueued_at = job["enqueued_at"]
104
+ if enqueued_at
105
+ if enqueued_at.is_a?(Float)
106
+ # old format
107
+ now = Time.now.to_f
108
+ now - enqueued_at
109
+ else
110
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
111
+ (now - enqueued_at) / 1000.0
112
+ end
113
+ else
114
+ 0.0
115
+ end
107
116
  else
108
- 0
117
+ 0.0
109
118
  end
110
119
 
111
120
  @stats = {
@@ -265,11 +274,22 @@ module Sidekiq
265
274
  entry = Sidekiq.redis { |conn|
266
275
  conn.lindex(@rname, -1)
267
276
  }
268
- return 0 unless entry
277
+ return 0.0 unless entry
278
+
269
279
  job = Sidekiq.load_json(entry)
270
- now = Time.now.to_f
271
- thence = job["enqueued_at"] || now
272
- now - thence
280
+ enqueued_at = job["enqueued_at"]
281
+ if enqueued_at
282
+ if enqueued_at.is_a?(Float)
283
+ # old format
284
+ now = Time.now.to_f
285
+ now - enqueued_at
286
+ else
287
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
288
+ (now - enqueued_at) / 1000.0
289
+ end
290
+ else
291
+ 0.0
292
+ end
273
293
  end
274
294
 
275
295
  def each
@@ -422,11 +442,13 @@ module Sidekiq
422
442
  end
423
443
 
424
444
  def enqueued_at
425
- self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
445
+ if self["enqueued_at"]
446
+ time_from_timestamp(self["enqueued_at"])
447
+ end
426
448
  end
427
449
 
428
450
  def created_at
429
- Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
451
+ time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
430
452
  end
431
453
 
432
454
  def tags
@@ -444,8 +466,17 @@ module Sidekiq
444
466
  end
445
467
 
446
468
  def latency
447
- now = Time.now.to_f
448
- now - (@item["enqueued_at"] || @item["created_at"] || now)
469
+ timestamp = @item["enqueued_at"] || @item["created_at"]
470
+ if timestamp
471
+ if timestamp.is_a?(Float)
472
+ # old format
473
+ Time.now.to_f - timestamp
474
+ else
475
+ (::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) - timestamp) / 1000.0
476
+ end
477
+ else
478
+ 0.0
479
+ end
449
480
  end
450
481
 
451
482
  # Remove this job from the queue
@@ -494,6 +525,15 @@ module Sidekiq
494
525
  uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
495
526
  Sidekiq.load_json(uncompressed)
496
527
  end
528
+
529
+ def time_from_timestamp(timestamp)
530
+ if timestamp.is_a?(Float)
531
+ # old format, timestamps were stored as fractional seconds since the epoch
532
+ Time.at(timestamp).utc
533
+ else
534
+ Time.at(timestamp / 1000, timestamp % 1000, :millisecond)
535
+ end
536
+ end
497
537
  end
498
538
 
499
539
  # Represents a job within a Redis sorted set where the score
@@ -1142,7 +1182,7 @@ module Sidekiq
1142
1182
  end
1143
1183
  end
1144
1184
 
1145
- results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1185
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1146
1186
  end
1147
1187
 
1148
1188
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1172,13 +1212,14 @@ module Sidekiq
1172
1212
  #
1173
1213
  # @param jid [String] the job identifier
1174
1214
  # @return [Sidekiq::Work] the work or nil
1175
- def find_work_by_jid(jid)
1215
+ def find_work(jid)
1176
1216
  each do |_process_id, _thread_id, work|
1177
1217
  job = work.job
1178
1218
  return work if job.jid == jid
1179
1219
  end
1180
1220
  nil
1181
1221
  end
1222
+ alias_method :find_work_by_jid, :find_work
1182
1223
  end
1183
1224
 
1184
1225
  # Sidekiq::Work represents a job which is currently executing.
@@ -1208,33 +1249,64 @@ module Sidekiq
1208
1249
  def payload
1209
1250
  @hsh["payload"]
1210
1251
  end
1252
+ end
1211
1253
 
1212
- # deprecated
1213
- def [](key)
1214
- kwargs = {uplevel: 1}
1215
- kwargs[:category] = :deprecated if RUBY_VERSION > "3.0" # TODO
1216
- warn("Direct access to `Sidekiq::Work` attributes is deprecated, please use `#payload`, `#queue`, `#run_at` or `#job` instead", **kwargs)
1254
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1255
+ # Is "worker" a process, a type of job, a thread? Undefined!
1256
+ # WorkSet better describes the data.
1257
+ Workers = WorkSet
1217
1258
 
1218
- @hsh[key]
1219
- end
1259
+ class ProfileSet
1260
+ include Enumerable
1220
1261
 
1221
- # :nodoc:
1222
- # @api private
1223
- def raw(name)
1224
- @hsh[name]
1262
+ # This is a point in time/snapshot API, you'll need to instantiate a new instance
1263
+ # if you want to fetch newer records.
1264
+ def initialize
1265
+ @records = Sidekiq.redis do |c|
1266
+ # This throws away expired profiles
1267
+ c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
1268
+ # retreive records, newest to oldest
1269
+ c.zrange("profiles", "+inf", 0, "byscore", "rev")
1270
+ end
1225
1271
  end
1226
1272
 
1227
- def method_missing(*all)
1228
- @hsh.send(*all)
1273
+ def size
1274
+ @records.size
1229
1275
  end
1230
1276
 
1231
- def respond_to_missing?(name, *args)
1232
- @hsh.respond_to?(name)
1277
+ def each(&block)
1278
+ fetch_keys = %w[started_at jid type token size elapsed].freeze
1279
+ arrays = Sidekiq.redis do |c|
1280
+ c.pipelined do |p|
1281
+ @records.each do |key|
1282
+ p.hmget(key, *fetch_keys)
1283
+ end
1284
+ end
1285
+ end
1286
+
1287
+ arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
1233
1288
  end
1234
1289
  end
1235
1290
 
1236
- # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1237
- # Is "worker" a process, a type of job, a thread? Undefined!
1238
- # WorkSet better describes the data.
1239
- Workers = WorkSet
1291
+ class ProfileRecord
1292
+ attr_reader :started_at, :jid, :type, :token, :size, :elapsed
1293
+
1294
+ def initialize(arr)
1295
+ # Must be same order as fetch_keys above
1296
+ @started_at = Time.at(Integer(arr[0]))
1297
+ @jid = arr[1]
1298
+ @type = arr[2]
1299
+ @token = arr[3]
1300
+ @size = Integer(arr[4])
1301
+ @elapsed = Float(arr[5])
1302
+ end
1303
+
1304
+ def key
1305
+ "#{token}-#{jid}"
1306
+ end
1307
+
1308
+ def data
1309
+ Sidekiq.redis { |c| c.hget(key, "data") }
1310
+ end
1311
+ end
1240
1312
  end
@@ -27,7 +27,7 @@ module Sidekiq
27
27
  attr_reader :mode
28
28
  attr_reader :weights
29
29
 
30
- def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
30
+ def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig, :thread_priority
31
31
 
32
32
  def initialize(name, config)
33
33
  @name = name
data/lib/sidekiq/cli.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  $stdout.sync = true
4
4
 
5
5
  require "yaml"
6
- require "singleton"
7
6
  require "optparse"
8
7
  require "erb"
9
8
  require "fileutils"
@@ -17,7 +16,6 @@ require "sidekiq/launcher"
17
16
  module Sidekiq # :nodoc:
18
17
  class CLI
19
18
  include Sidekiq::Component
20
- include Singleton unless $TESTING
21
19
 
22
20
  attr_accessor :launcher
23
21
  attr_accessor :environment
@@ -31,6 +29,10 @@ module Sidekiq # :nodoc:
31
29
  validate!
32
30
  end
33
31
 
32
+ def self.instance
33
+ @instance ||= new
34
+ end
35
+
34
36
  def jruby?
35
37
  defined?(::JRUBY_VERSION)
36
38
  end
@@ -74,7 +76,7 @@ module Sidekiq # :nodoc:
74
76
  # fire startup and start multithreading.
75
77
  info = @config.redis_info
76
78
  ver = Gem::Version.new(info["redis_version"])
77
- raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
79
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.2.0 or greater" if ver < Gem::Version.new("7.2.0")
78
80
 
79
81
  maxmemory_policy = info["maxmemory_policy"]
80
82
  if maxmemory_policy != "noeviction" && maxmemory_policy != ""
@@ -298,29 +300,18 @@ module Sidekiq # :nodoc:
298
300
 
299
301
  if File.directory?(@config[:require])
300
302
  require "rails"
301
- if ::Rails::VERSION::MAJOR < 6
302
- warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
303
+ if ::Rails::VERSION::MAJOR < 7
304
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 7+"
303
305
  end
304
306
  require "sidekiq/rails"
305
307
  require File.expand_path("#{@config[:require]}/config/environment.rb")
306
- @config[:tag] ||= default_tag
308
+ @config[:tag] ||= default_tag(::Rails.root)
307
309
  else
308
310
  require @config[:require]
311
+ @config[:tag] ||= default_tag
309
312
  end
310
313
  end
311
314
 
312
- def default_tag
313
- dir = ::Rails.root
314
- name = File.basename(dir)
315
- prevdir = File.dirname(dir) # Capistrano release directory?
316
- if name.to_i != 0 && prevdir
317
- if File.basename(prevdir) == "releases"
318
- return File.basename(File.dirname(prevdir))
319
- end
320
- end
321
- name
322
- end
323
-
324
315
  def validate!
325
316
  if !File.exist?(@config[:require]) ||
326
317
  (File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
@@ -395,7 +386,12 @@ module Sidekiq # :nodoc:
395
386
  end
396
387
 
397
388
  def initialize_logger
398
- @config.logger.level = ::Logger::DEBUG if @config[:verbose]
389
+ if @config[:verbose] || ENV["DEBUG_INVOCATION"] == "1"
390
+ # DEBUG_INVOCATION is a systemd-ism triggered by
391
+ # RestartMode=debug. We turn on debugging when the
392
+ # sidekiq process crashes and is restarted with this flag.
393
+ @config.logger.level = ::Logger::DEBUG
394
+ end
399
395
  end
400
396
 
401
397
  def parse_config(path)
@@ -67,9 +67,7 @@ module Sidekiq
67
67
  c.pipelined do |p|
68
68
  p.hsetnx(key, "cancelled", Time.now.to_i)
69
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")
70
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
73
71
  end
74
72
  end
75
73
  result.to_i
@@ -266,22 +264,21 @@ module Sidekiq
266
264
  if payloads.first.key?("at")
267
265
  conn.zadd("schedule", payloads.flat_map { |hash|
268
266
  at = hash["at"].to_s
269
- # ActiveJob sets this but the job has not been enqueued yet
270
- hash.delete("enqueued_at")
271
- # TODO: Use hash.except("at") when support for Ruby 2.7 is dropped
272
- hash = hash.dup
273
- hash.delete("at")
267
+ # ActiveJob sets enqueued_at but the job has not been enqueued yet
268
+ hash = hash.except("enqueued_at", "at")
274
269
  [at, Sidekiq.dump_json(hash)]
275
270
  })
276
271
  else
277
- queue = payloads.first["queue"]
278
- now = Time.now.to_f
279
- to_push = payloads.map { |entry|
280
- entry["enqueued_at"] = now
281
- Sidekiq.dump_json(entry)
282
- }
283
- conn.sadd("queues", [queue])
284
- conn.lpush("queue:#{queue}", to_push)
272
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) # milliseconds since the epoch
273
+ grouped_queues = payloads.group_by { |job| job["queue"] }
274
+ conn.sadd("queues", grouped_queues.keys)
275
+ grouped_queues.each do |queue, grouped_payloads|
276
+ to_push = grouped_payloads.map { |entry|
277
+ entry["enqueued_at"] = now
278
+ Sidekiq.dump_json(entry)
279
+ }
280
+ conn.lpush("queue:#{queue}", to_push)
281
+ end
285
282
  end
286
283
  end
287
284
  end