sidekiq 6.1.2 → 6.5.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.
Potentially problematic release.
This version of sidekiq might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Changes.md +215 -2
- data/LICENSE +3 -3
- data/README.md +9 -4
- data/bin/sidekiq +3 -3
- data/bin/sidekiqload +70 -66
- data/bin/sidekiqmon +1 -1
- data/lib/generators/sidekiq/job_generator.rb +57 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
- data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
- data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
- data/lib/sidekiq/api.rb +321 -145
- data/lib/sidekiq/cli.rb +73 -40
- data/lib/sidekiq/client.rb +48 -72
- data/lib/sidekiq/{util.rb → component.rb} +12 -14
- data/lib/sidekiq/delay.rb +3 -1
- data/lib/sidekiq/extensions/generic_proxy.rb +4 -2
- data/lib/sidekiq/fetch.rb +31 -20
- data/lib/sidekiq/job.rb +13 -0
- data/lib/sidekiq/job_logger.rb +16 -28
- data/lib/sidekiq/job_retry.rb +79 -59
- data/lib/sidekiq/job_util.rb +71 -0
- data/lib/sidekiq/launcher.rb +126 -65
- data/lib/sidekiq/logger.rb +11 -20
- data/lib/sidekiq/manager.rb +35 -34
- data/lib/sidekiq/metrics/deploy.rb +47 -0
- data/lib/sidekiq/metrics/query.rb +153 -0
- data/lib/sidekiq/metrics/shared.rb +94 -0
- data/lib/sidekiq/metrics/tracking.rb +134 -0
- data/lib/sidekiq/middleware/chain.rb +87 -41
- data/lib/sidekiq/middleware/current_attributes.rb +63 -0
- data/lib/sidekiq/middleware/i18n.rb +6 -4
- data/lib/sidekiq/middleware/modules.rb +21 -0
- data/lib/sidekiq/monitor.rb +1 -1
- data/lib/sidekiq/paginator.rb +8 -8
- data/lib/sidekiq/processor.rb +47 -41
- data/lib/sidekiq/rails.rb +22 -4
- data/lib/sidekiq/redis_client_adapter.rb +154 -0
- data/lib/sidekiq/redis_connection.rb +84 -55
- data/lib/sidekiq/ring_buffer.rb +29 -0
- data/lib/sidekiq/scheduled.rb +55 -25
- data/lib/sidekiq/testing/inline.rb +4 -4
- data/lib/sidekiq/testing.rb +38 -39
- data/lib/sidekiq/transaction_aware_client.rb +45 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +3 -3
- data/lib/sidekiq/web/application.rb +37 -13
- data/lib/sidekiq/web/csrf_protection.rb +30 -8
- data/lib/sidekiq/web/helpers.rb +60 -28
- data/lib/sidekiq/web/router.rb +4 -1
- data/lib/sidekiq/web.rb +38 -78
- data/lib/sidekiq/worker.rb +136 -13
- data/lib/sidekiq.rb +114 -31
- data/sidekiq.gemspec +12 -4
- data/web/assets/images/apple-touch-icon.png +0 -0
- data/web/assets/javascripts/application.js +113 -60
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard.js +50 -67
- data/web/assets/javascripts/graph.js +16 -0
- data/web/assets/javascripts/metrics.js +262 -0
- data/web/assets/stylesheets/application-dark.css +36 -36
- data/web/assets/stylesheets/application-rtl.css +0 -4
- data/web/assets/stylesheets/application.css +82 -237
- data/web/locales/ar.yml +8 -2
- data/web/locales/el.yml +43 -19
- data/web/locales/en.yml +11 -1
- data/web/locales/es.yml +18 -2
- data/web/locales/fr.yml +8 -1
- data/web/locales/ja.yml +3 -0
- data/web/locales/lt.yml +1 -1
- data/web/locales/pt-br.yml +27 -9
- data/web/views/_footer.erb +1 -1
- data/web/views/_job_info.erb +1 -1
- data/web/views/_nav.erb +1 -1
- data/web/views/_poll_link.erb +2 -5
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +50 -19
- data/web/views/dashboard.erb +23 -14
- data/web/views/dead.erb +1 -1
- data/web/views/layout.erb +2 -1
- data/web/views/metrics.erb +69 -0
- data/web/views/metrics_for_job.erb +87 -0
- data/web/views/morgue.erb +6 -6
- data/web/views/queue.erb +15 -11
- data/web/views/queues.erb +3 -3
- data/web/views/retries.erb +7 -7
- data/web/views/retry.erb +1 -1
- data/web/views/scheduled.erb +1 -1
- metadata +43 -36
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
- data/.github/contributing.md +0 -32
- data/.github/workflows/ci.yml +0 -41
- data/.gitignore +0 -13
- data/.standard.yml +0 -20
- data/3.0-Upgrade.md +0 -70
- data/4.0-Upgrade.md +0 -53
- data/5.0-Upgrade.md +0 -56
- data/6.0-Upgrade.md +0 -72
- data/COMM-LICENSE +0 -97
- data/Ent-2.0-Upgrade.md +0 -37
- data/Ent-Changes.md +0 -281
- data/Gemfile +0 -24
- data/Gemfile.lock +0 -192
- data/Pro-2.0-Upgrade.md +0 -138
- data/Pro-3.0-Upgrade.md +0 -44
- data/Pro-4.0-Upgrade.md +0 -35
- data/Pro-5.0-Upgrade.md +0 -25
- data/Pro-Changes.md +0 -805
- data/Rakefile +0 -10
- data/code_of_conduct.md +0 -50
- data/lib/generators/sidekiq/worker_generator.rb +0 -57
- data/lib/sidekiq/exception_handler.rb +0 -27
@@ -0,0 +1,29 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
class RingBuffer
|
5
|
+
include Enumerable
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@buf, :[], :each, :size
|
8
|
+
|
9
|
+
def initialize(size, default = 0)
|
10
|
+
@size = size
|
11
|
+
@buf = Array.new(size, default)
|
12
|
+
@index = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(element)
|
16
|
+
@buf[@index % @size] = element
|
17
|
+
@index += 1
|
18
|
+
element
|
19
|
+
end
|
20
|
+
|
21
|
+
def buffer
|
22
|
+
@buf
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset(default = 0)
|
26
|
+
@buf.fill(default)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/sidekiq/scheduled.rb
CHANGED
@@ -1,37 +1,63 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "sidekiq"
|
4
|
-
require "sidekiq/
|
5
|
-
require "sidekiq/api"
|
4
|
+
require "sidekiq/component"
|
6
5
|
|
7
6
|
module Sidekiq
|
8
7
|
module Scheduled
|
9
8
|
SETS = %w[retry schedule]
|
10
9
|
|
11
10
|
class Enq
|
12
|
-
|
11
|
+
LUA_ZPOPBYSCORE = <<~LUA
|
12
|
+
local key, now = KEYS[1], ARGV[1]
|
13
|
+
local jobs = redis.call("zrangebyscore", key, "-inf", now, "limit", 0, 1)
|
14
|
+
if jobs[1] then
|
15
|
+
redis.call("zrem", key, jobs[1])
|
16
|
+
return jobs[1]
|
17
|
+
end
|
18
|
+
LUA
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@done = false
|
22
|
+
@lua_zpopbyscore_sha = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def enqueue_jobs(sorted_sets = SETS)
|
13
26
|
# A job's "score" in Redis is the time at which it should be processed.
|
14
27
|
# Just check Redis for the set of jobs with a timestamp before now.
|
15
28
|
Sidekiq.redis do |conn|
|
16
29
|
sorted_sets.each do |sorted_set|
|
17
|
-
# Get next
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
# the queue, it's because another process already popped it so we can move on to the
|
25
|
-
# next one.
|
26
|
-
if conn.zrem(sorted_set, job)
|
27
|
-
Sidekiq::Client.push(Sidekiq.load_json(job))
|
28
|
-
Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
|
29
|
-
end
|
30
|
-
end
|
30
|
+
# Get next item in the queue with score (time to execute) <= now.
|
31
|
+
# We need to go through the list one at a time to reduce the risk of something
|
32
|
+
# going wrong between the time jobs are popped from the scheduled queue and when
|
33
|
+
# they are pushed onto a work queue and losing the jobs.
|
34
|
+
while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s]))
|
35
|
+
Sidekiq::Client.push(Sidekiq.load_json(job))
|
36
|
+
Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
|
31
37
|
end
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
41
|
+
|
42
|
+
def terminate
|
43
|
+
@done = true
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def zpopbyscore(conn, keys: nil, argv: nil)
|
49
|
+
if @lua_zpopbyscore_sha.nil?
|
50
|
+
raw_conn = conn.respond_to?(:redis) ? conn.redis : conn
|
51
|
+
@lua_zpopbyscore_sha = raw_conn.script(:load, LUA_ZPOPBYSCORE)
|
52
|
+
end
|
53
|
+
|
54
|
+
conn.evalsha(@lua_zpopbyscore_sha, keys, argv)
|
55
|
+
rescue RedisConnection.adapter::CommandError => e
|
56
|
+
raise unless e.message.start_with?("NOSCRIPT")
|
57
|
+
|
58
|
+
@lua_zpopbyscore_sha = nil
|
59
|
+
retry
|
60
|
+
end
|
35
61
|
end
|
36
62
|
|
37
63
|
##
|
@@ -40,20 +66,24 @@ module Sidekiq
|
|
40
66
|
# just pops the job back onto its original queue so the
|
41
67
|
# workers can pick it up like any other job.
|
42
68
|
class Poller
|
43
|
-
include
|
69
|
+
include Sidekiq::Component
|
44
70
|
|
45
71
|
INITIAL_WAIT = 10
|
46
72
|
|
47
|
-
def initialize
|
48
|
-
@
|
73
|
+
def initialize(options)
|
74
|
+
@config = options
|
75
|
+
@enq = (options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
|
49
76
|
@sleeper = ConnectionPool::TimedStack.new
|
50
77
|
@done = false
|
51
78
|
@thread = nil
|
79
|
+
@count_calls = 0
|
52
80
|
end
|
53
81
|
|
54
82
|
# Shut down this instance, will pause until the thread is dead.
|
55
83
|
def terminate
|
56
84
|
@done = true
|
85
|
+
@enq.terminate if @enq.respond_to?(:terminate)
|
86
|
+
|
57
87
|
if @thread
|
58
88
|
t = @thread
|
59
89
|
@thread = nil
|
@@ -70,7 +100,7 @@ module Sidekiq
|
|
70
100
|
enqueue
|
71
101
|
wait
|
72
102
|
end
|
73
|
-
|
103
|
+
logger.info("Scheduler exiting...")
|
74
104
|
}
|
75
105
|
end
|
76
106
|
|
@@ -141,18 +171,18 @@ module Sidekiq
|
|
141
171
|
#
|
142
172
|
# We only do this if poll_interval_average is unset (the default).
|
143
173
|
def poll_interval_average
|
144
|
-
|
174
|
+
@config[:poll_interval_average] ||= scaled_poll_interval
|
145
175
|
end
|
146
176
|
|
147
177
|
# Calculates an average poll interval based on the number of known Sidekiq processes.
|
148
178
|
# This minimizes a single point of failure by dispersing check-ins but without taxing
|
149
179
|
# Redis if you run many Sidekiq processes.
|
150
180
|
def scaled_poll_interval
|
151
|
-
process_count *
|
181
|
+
process_count * @config[:average_scheduled_poll_interval]
|
152
182
|
end
|
153
183
|
|
154
184
|
def process_count
|
155
|
-
pcount = Sidekiq
|
185
|
+
pcount = Sidekiq.redis { |conn| conn.scard("processes") }
|
156
186
|
pcount = 1 if pcount == 0
|
157
187
|
pcount
|
158
188
|
end
|
@@ -162,7 +192,7 @@ module Sidekiq
|
|
162
192
|
# to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
|
163
193
|
# of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
|
164
194
|
total = 0
|
165
|
-
total += INITIAL_WAIT unless
|
195
|
+
total += INITIAL_WAIT unless @config[:poll_interval_average]
|
166
196
|
total += (5 * rand)
|
167
197
|
|
168
198
|
@sleeper.pop(total)
|
@@ -4,7 +4,7 @@ require "sidekiq/testing"
|
|
4
4
|
|
5
5
|
##
|
6
6
|
# The Sidekiq inline infrastructure overrides perform_async so that it
|
7
|
-
# actually calls perform instead. This allows
|
7
|
+
# actually calls perform instead. This allows jobs to be run inline in a
|
8
8
|
# testing environment.
|
9
9
|
#
|
10
10
|
# This is similar to `Resque.inline = true` functionality.
|
@@ -15,8 +15,8 @@ require "sidekiq/testing"
|
|
15
15
|
#
|
16
16
|
# $external_variable = 0
|
17
17
|
#
|
18
|
-
# class
|
19
|
-
# include Sidekiq::
|
18
|
+
# class ExternalJob
|
19
|
+
# include Sidekiq::Job
|
20
20
|
#
|
21
21
|
# def perform
|
22
22
|
# $external_variable = 1
|
@@ -24,7 +24,7 @@ require "sidekiq/testing"
|
|
24
24
|
# end
|
25
25
|
#
|
26
26
|
# assert_equal 0, $external_variable
|
27
|
-
#
|
27
|
+
# ExternalJob.perform_async
|
28
28
|
# assert_equal 1, $external_variable
|
29
29
|
#
|
30
30
|
Sidekiq::Testing.inline!
|
data/lib/sidekiq/testing.rb
CHANGED
@@ -101,20 +101,20 @@ module Sidekiq
|
|
101
101
|
##
|
102
102
|
# The Queues class is only for testing the fake queue implementation.
|
103
103
|
# There are 2 data structures involved in tandem. This is due to the
|
104
|
-
# Rspec syntax of change(
|
104
|
+
# Rspec syntax of change(HardJob.jobs, :size). It keeps a reference
|
105
105
|
# to the array. Because the array was dervied from a filter of the total
|
106
106
|
# jobs enqueued, it appeared as though the array didn't change.
|
107
107
|
#
|
108
108
|
# To solve this, we'll keep 2 hashes containing the jobs. One with keys based
|
109
|
-
# on the queue, and another with keys of the
|
110
|
-
#
|
109
|
+
# on the queue, and another with keys of the job type, so the array for
|
110
|
+
# HardJob.jobs is a straight reference to a real array.
|
111
111
|
#
|
112
112
|
# Queue-based hash:
|
113
113
|
#
|
114
114
|
# {
|
115
115
|
# "default"=>[
|
116
116
|
# {
|
117
|
-
# "class"=>"TestTesting::
|
117
|
+
# "class"=>"TestTesting::HardJob",
|
118
118
|
# "args"=>[1, 2],
|
119
119
|
# "retry"=>true,
|
120
120
|
# "queue"=>"default",
|
@@ -124,12 +124,12 @@ module Sidekiq
|
|
124
124
|
# ]
|
125
125
|
# }
|
126
126
|
#
|
127
|
-
#
|
127
|
+
# Job-based hash:
|
128
128
|
#
|
129
129
|
# {
|
130
|
-
# "TestTesting::
|
130
|
+
# "TestTesting::HardJob"=>[
|
131
131
|
# {
|
132
|
-
# "class"=>"TestTesting::
|
132
|
+
# "class"=>"TestTesting::HardJob",
|
133
133
|
# "args"=>[1, 2],
|
134
134
|
# "retry"=>true,
|
135
135
|
# "queue"=>"default",
|
@@ -144,14 +144,14 @@ module Sidekiq
|
|
144
144
|
# require 'sidekiq/testing'
|
145
145
|
#
|
146
146
|
# assert_equal 0, Sidekiq::Queues["default"].size
|
147
|
-
#
|
147
|
+
# HardJob.perform_async(:something)
|
148
148
|
# assert_equal 1, Sidekiq::Queues["default"].size
|
149
149
|
# assert_equal :something, Sidekiq::Queues["default"].first['args'][0]
|
150
150
|
#
|
151
|
-
# You can also clear all
|
151
|
+
# You can also clear all jobs:
|
152
152
|
#
|
153
153
|
# assert_equal 0, Sidekiq::Queues["default"].size
|
154
|
-
#
|
154
|
+
# HardJob.perform_async(:something)
|
155
155
|
# Sidekiq::Queues.clear_all
|
156
156
|
# assert_equal 0, Sidekiq::Queues["default"].size
|
157
157
|
#
|
@@ -170,35 +170,36 @@ module Sidekiq
|
|
170
170
|
|
171
171
|
def push(queue, klass, job)
|
172
172
|
jobs_by_queue[queue] << job
|
173
|
-
|
173
|
+
jobs_by_class[klass] << job
|
174
174
|
end
|
175
175
|
|
176
176
|
def jobs_by_queue
|
177
177
|
@jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] }
|
178
178
|
end
|
179
179
|
|
180
|
-
def
|
181
|
-
@
|
180
|
+
def jobs_by_class
|
181
|
+
@jobs_by_class ||= Hash.new { |hash, key| hash[key] = [] }
|
182
182
|
end
|
183
|
+
alias_method :jobs_by_worker, :jobs_by_class
|
183
184
|
|
184
185
|
def delete_for(jid, queue, klass)
|
185
186
|
jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid }
|
186
|
-
|
187
|
+
jobs_by_class[klass].delete_if { |job| job["jid"] == jid }
|
187
188
|
end
|
188
189
|
|
189
190
|
def clear_for(queue, klass)
|
190
|
-
jobs_by_queue[queue].clear
|
191
|
-
|
191
|
+
jobs_by_queue[queue.to_s].clear
|
192
|
+
jobs_by_class[klass].clear
|
192
193
|
end
|
193
194
|
|
194
195
|
def clear_all
|
195
196
|
jobs_by_queue.clear
|
196
|
-
|
197
|
+
jobs_by_class.clear
|
197
198
|
end
|
198
199
|
end
|
199
200
|
end
|
200
201
|
|
201
|
-
module
|
202
|
+
module Job
|
202
203
|
##
|
203
204
|
# The Sidekiq testing infrastructure overrides perform_async
|
204
205
|
# so that it does not actually touch the network. Instead it
|
@@ -212,16 +213,16 @@ module Sidekiq
|
|
212
213
|
#
|
213
214
|
# require 'sidekiq/testing'
|
214
215
|
#
|
215
|
-
# assert_equal 0,
|
216
|
-
#
|
217
|
-
# assert_equal 1,
|
218
|
-
# assert_equal :something,
|
216
|
+
# assert_equal 0, HardJob.jobs.size
|
217
|
+
# HardJob.perform_async(:something)
|
218
|
+
# assert_equal 1, HardJob.jobs.size
|
219
|
+
# assert_equal :something, HardJob.jobs[0]['args'][0]
|
219
220
|
#
|
220
221
|
# assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
|
221
222
|
# MyMailer.delay.send_welcome_email('foo@example.com')
|
222
223
|
# assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size
|
223
224
|
#
|
224
|
-
# You can also clear and drain all
|
225
|
+
# You can also clear and drain all job types:
|
225
226
|
#
|
226
227
|
# assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
|
227
228
|
# assert_equal 0, Sidekiq::Extensions::DelayedModel.jobs.size
|
@@ -241,14 +242,14 @@ module Sidekiq
|
|
241
242
|
#
|
242
243
|
# RSpec.configure do |config|
|
243
244
|
# config.before(:each) do
|
244
|
-
# Sidekiq::
|
245
|
+
# Sidekiq::Job.clear_all
|
245
246
|
# end
|
246
247
|
# end
|
247
248
|
#
|
248
249
|
# or for acceptance testing, i.e. with cucumber:
|
249
250
|
#
|
250
251
|
# AfterStep do
|
251
|
-
# Sidekiq::
|
252
|
+
# Sidekiq::Job.drain_all
|
252
253
|
# end
|
253
254
|
#
|
254
255
|
# When I sign up as "foo@example.com"
|
@@ -262,7 +263,7 @@ module Sidekiq
|
|
262
263
|
|
263
264
|
# Jobs queued for this worker
|
264
265
|
def jobs
|
265
|
-
Queues.
|
266
|
+
Queues.jobs_by_class[to_s]
|
266
267
|
end
|
267
268
|
|
268
269
|
# Clear all jobs for this worker
|
@@ -288,11 +289,11 @@ module Sidekiq
|
|
288
289
|
end
|
289
290
|
|
290
291
|
def process_job(job)
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
Sidekiq::Testing.server_middleware.invoke(
|
295
|
-
execute_job(
|
292
|
+
inst = new
|
293
|
+
inst.jid = job["jid"]
|
294
|
+
inst.bid = job["bid"] if inst.respond_to?(:bid=)
|
295
|
+
Sidekiq::Testing.server_middleware.invoke(inst, job, job["queue"]) do
|
296
|
+
execute_job(inst, job["args"])
|
296
297
|
end
|
297
298
|
end
|
298
299
|
|
@@ -306,18 +307,18 @@ module Sidekiq
|
|
306
307
|
Queues.jobs_by_queue.values.flatten
|
307
308
|
end
|
308
309
|
|
309
|
-
# Clear all queued jobs
|
310
|
+
# Clear all queued jobs
|
310
311
|
def clear_all
|
311
312
|
Queues.clear_all
|
312
313
|
end
|
313
314
|
|
314
|
-
# Drain all queued jobs
|
315
|
+
# Drain (execute) all queued jobs
|
315
316
|
def drain_all
|
316
317
|
while jobs.any?
|
317
|
-
|
318
|
+
job_classes = jobs.map { |job| job["class"] }.uniq
|
318
319
|
|
319
|
-
|
320
|
-
Sidekiq::Testing.constantize(
|
320
|
+
job_classes.each do |job_class|
|
321
|
+
Sidekiq::Testing.constantize(job_class).drain
|
321
322
|
end
|
322
323
|
end
|
323
324
|
end
|
@@ -338,7 +339,5 @@ module Sidekiq
|
|
338
339
|
end
|
339
340
|
|
340
341
|
if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
|
341
|
-
|
342
|
-
puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
|
343
|
-
puts("**************************************************")
|
342
|
+
warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
|
344
343
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require "sidekiq/client"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
class TransactionAwareClient
|
8
|
+
def initialize(redis_pool)
|
9
|
+
@redis_client = Client.new(redis_pool)
|
10
|
+
end
|
11
|
+
|
12
|
+
def push(item)
|
13
|
+
# pre-allocate the JID so we can return it immediately and
|
14
|
+
# save it to the database as part of the transaction.
|
15
|
+
item["jid"] ||= SecureRandom.hex(12)
|
16
|
+
AfterCommitEverywhere.after_commit { @redis_client.push(item) }
|
17
|
+
item["jid"]
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# We don't provide transactionality for push_bulk because we don't want
|
22
|
+
# to hold potentially hundreds of thousands of job records in memory due to
|
23
|
+
# a long running enqueue process.
|
24
|
+
def push_bulk(items)
|
25
|
+
@redis_client.push_bulk(items)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Use `Sidekiq.transactional_push!` in your sidekiq.rb initializer
|
32
|
+
module Sidekiq
|
33
|
+
def self.transactional_push!
|
34
|
+
begin
|
35
|
+
require "after_commit_everywhere"
|
36
|
+
rescue LoadError
|
37
|
+
Sidekiq.logger.error("You need to add after_commit_everywhere to your Gemfile to use Sidekiq's transactional client")
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
|
41
|
+
default_job_options["client_class"] = Sidekiq::TransactionAwareClient
|
42
|
+
Sidekiq::JobUtil::TRANSIENT_ATTRIBUTES << "client_class"
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
@@ -15,11 +15,11 @@ module Sidekiq
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def halt(res)
|
18
|
-
throw :halt, res
|
18
|
+
throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
|
19
19
|
end
|
20
20
|
|
21
21
|
def redirect(location)
|
22
|
-
throw :halt, [302, {"
|
22
|
+
throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
|
23
23
|
end
|
24
24
|
|
25
25
|
def params
|
@@ -68,7 +68,7 @@ module Sidekiq
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def json(payload)
|
71
|
-
[200, {"
|
71
|
+
[200, {"content-type" => "application/json", "cache-control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
|
72
72
|
end
|
73
73
|
|
74
74
|
def initialize(env, block)
|
@@ -4,7 +4,6 @@ module Sidekiq
|
|
4
4
|
class WebApplication
|
5
5
|
extend WebRouter
|
6
6
|
|
7
|
-
CONTENT_LENGTH = "Content-Length"
|
8
7
|
REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
|
9
8
|
CSP_HEADER = [
|
10
9
|
"default-src 'self' https: http:",
|
@@ -42,15 +41,38 @@ module Sidekiq
|
|
42
41
|
# nothing, backwards compatibility
|
43
42
|
end
|
44
43
|
|
44
|
+
head "/" do
|
45
|
+
# HEAD / is the cheapest heartbeat possible,
|
46
|
+
# it hits Redis to ensure connectivity
|
47
|
+
Sidekiq.redis { |c| c.llen("queue:default") }
|
48
|
+
""
|
49
|
+
end
|
50
|
+
|
45
51
|
get "/" do
|
46
52
|
@redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
47
|
-
|
53
|
+
days = (params["days"] || 30).to_i
|
54
|
+
return halt(401) if days < 1 || days > 180
|
55
|
+
|
56
|
+
stats_history = Sidekiq::Stats::History.new(days)
|
48
57
|
@processed_history = stats_history.processed
|
49
58
|
@failed_history = stats_history.failed
|
50
59
|
|
51
60
|
erb(:dashboard)
|
52
61
|
end
|
53
62
|
|
63
|
+
get "/metrics" do
|
64
|
+
q = Sidekiq::Metrics::Query.new
|
65
|
+
@query_result = q.top_jobs
|
66
|
+
erb(:metrics)
|
67
|
+
end
|
68
|
+
|
69
|
+
get "/metrics/:name" do
|
70
|
+
@name = route_params[:name]
|
71
|
+
q = Sidekiq::Metrics::Query.new
|
72
|
+
@query_result = q.for_job(@name)
|
73
|
+
erb(:metrics_for_job)
|
74
|
+
end
|
75
|
+
|
54
76
|
get "/busy" do
|
55
77
|
erb(:busy)
|
56
78
|
end
|
@@ -76,15 +98,17 @@ module Sidekiq
|
|
76
98
|
erb(:queues)
|
77
99
|
end
|
78
100
|
|
101
|
+
QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
|
102
|
+
|
79
103
|
get "/queues/:name" do
|
80
104
|
@name = route_params[:name]
|
81
105
|
|
82
|
-
halt(404)
|
106
|
+
halt(404) if !@name || @name !~ QUEUE_NAME
|
83
107
|
|
84
108
|
@count = (params["count"] || 25).to_i
|
85
109
|
@queue = Sidekiq::Queue.new(@name)
|
86
|
-
(@current_page, @total_size, @
|
87
|
-
@
|
110
|
+
(@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
|
111
|
+
@jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
|
88
112
|
|
89
113
|
erb(:queue)
|
90
114
|
end
|
@@ -105,7 +129,7 @@ module Sidekiq
|
|
105
129
|
|
106
130
|
post "/queues/:name/delete" do
|
107
131
|
name = route_params[:name]
|
108
|
-
Sidekiq::
|
132
|
+
Sidekiq::JobRecord.new(params["key_val"], name).delete
|
109
133
|
|
110
134
|
redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
|
111
135
|
end
|
@@ -288,10 +312,10 @@ module Sidekiq
|
|
288
312
|
|
289
313
|
def call(env)
|
290
314
|
action = self.class.match(env)
|
291
|
-
return [404, {"
|
315
|
+
return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
|
292
316
|
|
293
317
|
app = @klass
|
294
|
-
resp = catch(:halt) do
|
318
|
+
resp = catch(:halt) do
|
295
319
|
self.class.run_befores(app, action)
|
296
320
|
action.instance_exec env, &action.block
|
297
321
|
ensure
|
@@ -305,10 +329,10 @@ module Sidekiq
|
|
305
329
|
else
|
306
330
|
# rendered content goes here
|
307
331
|
headers = {
|
308
|
-
"
|
309
|
-
"
|
310
|
-
"
|
311
|
-
"
|
332
|
+
"content-type" => "text/html",
|
333
|
+
"cache-control" => "private, no-store",
|
334
|
+
"content-language" => action.locale,
|
335
|
+
"content-security-policy" => CSP_HEADER
|
312
336
|
}
|
313
337
|
# we'll let Rack calculate Content-Length for us.
|
314
338
|
[200, headers, [resp]]
|
@@ -316,7 +340,7 @@ module Sidekiq
|
|
316
340
|
end
|
317
341
|
|
318
342
|
def self.helpers(mod = nil, &block)
|
319
|
-
if
|
343
|
+
if block
|
320
344
|
WebAction.class_eval(&block)
|
321
345
|
else
|
322
346
|
WebAction.send(:include, mod)
|
@@ -66,7 +66,31 @@ module Sidekiq
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def session(env)
|
69
|
-
env["rack.session"] || fail(
|
69
|
+
env["rack.session"] || fail(<<~EOM)
|
70
|
+
Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
|
71
|
+
make sure you mount Sidekiq::Web *inside* your application routes:
|
72
|
+
|
73
|
+
|
74
|
+
Rails.application.routes.draw do
|
75
|
+
mount Sidekiq::Web => "/sidekiq"
|
76
|
+
....
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
If this is a Rails app in API mode, you need to enable sessions.
|
81
|
+
|
82
|
+
https://guides.rubyonrails.org/api_app.html#using-session-middlewares
|
83
|
+
|
84
|
+
If this is a bare Rack app, use a session middleware before Sidekiq::Web:
|
85
|
+
|
86
|
+
# first, use IRB to create a shared secret key for sessions and commit it
|
87
|
+
require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
|
88
|
+
|
89
|
+
# now use the secret with a session cookie middleware
|
90
|
+
use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
|
91
|
+
run Sidekiq::Web
|
92
|
+
|
93
|
+
EOM
|
70
94
|
end
|
71
95
|
|
72
96
|
def accept?(env)
|
@@ -90,13 +114,11 @@ module Sidekiq
|
|
90
114
|
end
|
91
115
|
|
92
116
|
sess = session(env)
|
93
|
-
|
94
|
-
# Checks that Rack::Session::Cookie did not return empty session
|
95
|
-
# object in case the digest verification failed
|
96
|
-
return false if sess.empty?
|
97
|
-
|
98
117
|
localtoken = sess[:csrf]
|
99
118
|
|
119
|
+
# Checks that Rack::Session::Cookie actualy contains the csrf toekn
|
120
|
+
return false if localtoken.nil?
|
121
|
+
|
100
122
|
# Rotate the session token after every use
|
101
123
|
sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
|
102
124
|
|
@@ -121,7 +143,7 @@ module Sidekiq
|
|
121
143
|
one_time_pad = SecureRandom.random_bytes(token.length)
|
122
144
|
encrypted_token = xor_byte_strings(one_time_pad, token)
|
123
145
|
masked_token = one_time_pad + encrypted_token
|
124
|
-
Base64.
|
146
|
+
Base64.urlsafe_encode64(masked_token)
|
125
147
|
end
|
126
148
|
|
127
149
|
# Essentially the inverse of +mask_token+.
|
@@ -147,7 +169,7 @@ module Sidekiq
|
|
147
169
|
end
|
148
170
|
|
149
171
|
def decode_token(token)
|
150
|
-
Base64.
|
172
|
+
Base64.urlsafe_decode64(token)
|
151
173
|
end
|
152
174
|
|
153
175
|
def xor_byte_strings(s1, s2)
|