sidekiq 5.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.github/contributing.md +32 -0
  3. data/.github/issue_template.md +9 -0
  4. data/.gitignore +13 -0
  5. data/.travis.yml +18 -0
  6. data/3.0-Upgrade.md +70 -0
  7. data/4.0-Upgrade.md +53 -0
  8. data/5.0-Upgrade.md +56 -0
  9. data/COMM-LICENSE +95 -0
  10. data/Changes.md +1402 -0
  11. data/Ent-Changes.md +174 -0
  12. data/Gemfile +29 -0
  13. data/LICENSE +9 -0
  14. data/Pro-2.0-Upgrade.md +138 -0
  15. data/Pro-3.0-Upgrade.md +44 -0
  16. data/Pro-Changes.md +632 -0
  17. data/README.md +107 -0
  18. data/Rakefile +12 -0
  19. data/bin/sidekiq +18 -0
  20. data/bin/sidekiqctl +99 -0
  21. data/bin/sidekiqload +149 -0
  22. data/code_of_conduct.md +50 -0
  23. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  24. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  25. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  26. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  27. data/lib/sidekiq.rb +228 -0
  28. data/lib/sidekiq/api.rb +871 -0
  29. data/lib/sidekiq/cli.rb +413 -0
  30. data/lib/sidekiq/client.rb +238 -0
  31. data/lib/sidekiq/core_ext.rb +119 -0
  32. data/lib/sidekiq/delay.rb +21 -0
  33. data/lib/sidekiq/exception_handler.rb +31 -0
  34. data/lib/sidekiq/extensions/action_mailer.rb +57 -0
  35. data/lib/sidekiq/extensions/active_record.rb +40 -0
  36. data/lib/sidekiq/extensions/class_methods.rb +40 -0
  37. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  38. data/lib/sidekiq/fetch.rb +81 -0
  39. data/lib/sidekiq/job_logger.rb +27 -0
  40. data/lib/sidekiq/job_retry.rb +235 -0
  41. data/lib/sidekiq/launcher.rb +167 -0
  42. data/lib/sidekiq/logging.rb +106 -0
  43. data/lib/sidekiq/manager.rb +138 -0
  44. data/lib/sidekiq/middleware/chain.rb +150 -0
  45. data/lib/sidekiq/middleware/i18n.rb +42 -0
  46. data/lib/sidekiq/middleware/server/active_record.rb +22 -0
  47. data/lib/sidekiq/paginator.rb +43 -0
  48. data/lib/sidekiq/processor.rb +238 -0
  49. data/lib/sidekiq/rails.rb +60 -0
  50. data/lib/sidekiq/redis_connection.rb +106 -0
  51. data/lib/sidekiq/scheduled.rb +147 -0
  52. data/lib/sidekiq/testing.rb +324 -0
  53. data/lib/sidekiq/testing/inline.rb +29 -0
  54. data/lib/sidekiq/util.rb +63 -0
  55. data/lib/sidekiq/version.rb +4 -0
  56. data/lib/sidekiq/web.rb +213 -0
  57. data/lib/sidekiq/web/action.rb +89 -0
  58. data/lib/sidekiq/web/application.rb +331 -0
  59. data/lib/sidekiq/web/helpers.rb +286 -0
  60. data/lib/sidekiq/web/router.rb +100 -0
  61. data/lib/sidekiq/worker.rb +144 -0
  62. data/sidekiq.gemspec +32 -0
  63. data/web/assets/images/favicon.ico +0 -0
  64. data/web/assets/images/logo.png +0 -0
  65. data/web/assets/images/status.png +0 -0
  66. data/web/assets/javascripts/application.js +92 -0
  67. data/web/assets/javascripts/dashboard.js +298 -0
  68. data/web/assets/stylesheets/application-rtl.css +246 -0
  69. data/web/assets/stylesheets/application.css +1111 -0
  70. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  71. data/web/assets/stylesheets/bootstrap.css +5 -0
  72. data/web/locales/ar.yml +80 -0
  73. data/web/locales/cs.yml +78 -0
  74. data/web/locales/da.yml +68 -0
  75. data/web/locales/de.yml +69 -0
  76. data/web/locales/el.yml +68 -0
  77. data/web/locales/en.yml +79 -0
  78. data/web/locales/es.yml +69 -0
  79. data/web/locales/fa.yml +80 -0
  80. data/web/locales/fr.yml +78 -0
  81. data/web/locales/he.yml +79 -0
  82. data/web/locales/hi.yml +75 -0
  83. data/web/locales/it.yml +69 -0
  84. data/web/locales/ja.yml +78 -0
  85. data/web/locales/ko.yml +68 -0
  86. data/web/locales/nb.yml +77 -0
  87. data/web/locales/nl.yml +68 -0
  88. data/web/locales/pl.yml +59 -0
  89. data/web/locales/pt-br.yml +68 -0
  90. data/web/locales/pt.yml +67 -0
  91. data/web/locales/ru.yml +78 -0
  92. data/web/locales/sv.yml +68 -0
  93. data/web/locales/ta.yml +75 -0
  94. data/web/locales/uk.yml +76 -0
  95. data/web/locales/ur.yml +80 -0
  96. data/web/locales/zh-cn.yml +68 -0
  97. data/web/locales/zh-tw.yml +68 -0
  98. data/web/views/_footer.erb +17 -0
  99. data/web/views/_job_info.erb +88 -0
  100. data/web/views/_nav.erb +66 -0
  101. data/web/views/_paging.erb +23 -0
  102. data/web/views/_poll_link.erb +7 -0
  103. data/web/views/_status.erb +4 -0
  104. data/web/views/_summary.erb +40 -0
  105. data/web/views/busy.erb +94 -0
  106. data/web/views/dashboard.erb +75 -0
  107. data/web/views/dead.erb +34 -0
  108. data/web/views/layout.erb +40 -0
  109. data/web/views/morgue.erb +75 -0
  110. data/web/views/queue.erb +45 -0
  111. data/web/views/queues.erb +28 -0
  112. data/web/views/retries.erb +76 -0
  113. data/web/views/retry.erb +34 -0
  114. data/web/views/scheduled.erb +54 -0
  115. data/web/views/scheduled_job_info.erb +8 -0
  116. metadata +366 -0
@@ -0,0 +1,6 @@
1
+ require 'rails_helper'
2
+ <% module_namespacing do -%>
3
+ RSpec.describe <%= class_name %>Worker, type: :worker do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
6
+ <% end -%>
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+ <% module_namespacing do -%>
3
+ class <%= class_name %>WorkerTest < <% if defined? Minitest::Test %>Minitest::Test<% else %>MiniTest::Unit::TestCase<% end %>
4
+ def test_example
5
+ skip "add some examples to (or delete) #{__FILE__}"
6
+ end
7
+ end
8
+ <% end -%>
@@ -0,0 +1,49 @@
1
+ require 'rails/generators/named_base'
2
+
3
+ module Sidekiq
4
+ module Generators # :nodoc:
5
+ class WorkerGenerator < ::Rails::Generators::NamedBase # :nodoc:
6
+ desc 'This generator creates a Sidekiq Worker in app/workers and a corresponding test'
7
+
8
+ check_class_collision suffix: 'Worker'
9
+
10
+ def self.default_generator_root
11
+ File.dirname(__FILE__)
12
+ end
13
+
14
+ def create_worker_file
15
+ template 'worker.rb.erb', File.join('app/workers', class_path, "#{file_name}_worker.rb")
16
+ end
17
+
18
+ def create_test_file
19
+ if defined?(RSpec)
20
+ create_worker_spec
21
+ else
22
+ create_worker_test
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def create_worker_spec
29
+ template_file = File.join(
30
+ 'spec/workers',
31
+ class_path,
32
+ "#{file_name}_worker_spec.rb"
33
+ )
34
+ template 'worker_spec.rb.erb', template_file
35
+ end
36
+
37
+ def create_worker_test
38
+ template_file = File.join(
39
+ 'test/workers',
40
+ class_path,
41
+ "#{file_name}_worker_test.rb"
42
+ )
43
+ template 'worker_test.rb.erb', template_file
44
+ end
45
+
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,228 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ require 'sidekiq/version'
4
+ fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.2.2." if RUBY_PLATFORM != 'java' && RUBY_VERSION < '2.2.2'
5
+
6
+ require 'sidekiq/logging'
7
+ require 'sidekiq/client'
8
+ require 'sidekiq/worker'
9
+ require 'sidekiq/redis_connection'
10
+ require 'sidekiq/delay'
11
+
12
+ require 'json'
13
+
14
+ module Sidekiq
15
+ NAME = 'Sidekiq'.freeze
16
+ LICENSE = 'See LICENSE and the LGPL-3.0 for licensing details.'
17
+
18
+ DEFAULTS = {
19
+ queues: [],
20
+ labels: [],
21
+ concurrency: 25,
22
+ require: '.',
23
+ environment: nil,
24
+ timeout: 8,
25
+ poll_interval_average: nil,
26
+ average_scheduled_poll_interval: 15,
27
+ error_handlers: [],
28
+ lifecycle_events: {
29
+ startup: [],
30
+ quiet: [],
31
+ shutdown: [],
32
+ heartbeat: [],
33
+ },
34
+ dead_max_jobs: 10_000,
35
+ dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
36
+ reloader: proc { |&block| block.call },
37
+ }
38
+
39
+ DEFAULT_WORKER_OPTIONS = {
40
+ 'retry' => true,
41
+ 'queue' => 'default'
42
+ }
43
+
44
+ FAKE_INFO = {
45
+ "redis_version" => "9.9.9",
46
+ "uptime_in_days" => "9999",
47
+ "connected_clients" => "9999",
48
+ "used_memory_human" => "9P",
49
+ "used_memory_peak_human" => "9P"
50
+ }.freeze
51
+
52
+ def self.❨╯°□°❩╯︵┻━┻
53
+ puts "Calm down, yo."
54
+ end
55
+
56
+ def self.options
57
+ @options ||= DEFAULTS.dup
58
+ end
59
+ def self.options=(opts)
60
+ @options = opts
61
+ end
62
+
63
+ ##
64
+ # Configuration for Sidekiq server, use like:
65
+ #
66
+ # Sidekiq.configure_server do |config|
67
+ # config.redis = { :namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/0' }
68
+ # config.server_middleware do |chain|
69
+ # chain.add MyServerHook
70
+ # end
71
+ # end
72
+ def self.configure_server
73
+ yield self if server?
74
+ end
75
+
76
+ ##
77
+ # Configuration for Sidekiq client, use like:
78
+ #
79
+ # Sidekiq.configure_client do |config|
80
+ # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/0' }
81
+ # end
82
+ def self.configure_client
83
+ yield self unless server?
84
+ end
85
+
86
+ def self.server?
87
+ defined?(Sidekiq::CLI)
88
+ end
89
+
90
+ def self.redis
91
+ raise ArgumentError, "requires a block" unless block_given?
92
+ redis_pool.with do |conn|
93
+ retryable = true
94
+ begin
95
+ yield conn
96
+ rescue Redis::CommandError => ex
97
+ #2550 Failover can cause the server to become a slave, need
98
+ # to disconnect and reopen the socket to get back to the master.
99
+ (conn.disconnect!; retryable = false; retry) if retryable && ex.message =~ /READONLY/
100
+ raise
101
+ end
102
+ end
103
+ end
104
+
105
+ def self.redis_info
106
+ redis do |conn|
107
+ begin
108
+ # admin commands can't go through redis-namespace starting
109
+ # in redis-namespace 2.0
110
+ if conn.respond_to?(:namespace)
111
+ conn.redis.info
112
+ else
113
+ conn.info
114
+ end
115
+ rescue Redis::CommandError => ex
116
+ #2850 return fake version when INFO command has (probably) been renamed
117
+ raise unless ex.message =~ /unknown command/
118
+ FAKE_INFO
119
+ end
120
+ end
121
+ end
122
+
123
+ def self.redis_pool
124
+ @redis ||= Sidekiq::RedisConnection.create
125
+ end
126
+
127
+ def self.redis=(hash)
128
+ @redis = if hash.is_a?(ConnectionPool)
129
+ hash
130
+ else
131
+ Sidekiq::RedisConnection.create(hash)
132
+ end
133
+ end
134
+
135
+ def self.client_middleware
136
+ @client_chain ||= Middleware::Chain.new
137
+ yield @client_chain if block_given?
138
+ @client_chain
139
+ end
140
+
141
+ def self.server_middleware
142
+ @server_chain ||= default_server_middleware
143
+ yield @server_chain if block_given?
144
+ @server_chain
145
+ end
146
+
147
+ def self.default_server_middleware
148
+ Middleware::Chain.new
149
+ end
150
+
151
+ def self.default_worker_options=(hash)
152
+ @default_worker_options = default_worker_options.merge(hash.stringify_keys)
153
+ end
154
+ def self.default_worker_options
155
+ defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
156
+ end
157
+
158
+ # Sidekiq.configure_server do |config|
159
+ # config.default_retries_exhausted = -> (job, ex) do
160
+ # end
161
+ # end
162
+ def self.default_retries_exhausted=(prok)
163
+ @default_retries_exhausted = prok
164
+ end
165
+ @default_retries_exhausted = ->(job, ex) { }
166
+ def self.default_retries_exhausted
167
+ @default_retries_exhausted
168
+ end
169
+
170
+ def self.load_json(string)
171
+ JSON.parse(string)
172
+ end
173
+ def self.dump_json(object)
174
+ JSON.generate(object)
175
+ end
176
+
177
+ def self.logger
178
+ Sidekiq::Logging.logger
179
+ end
180
+ def self.logger=(log)
181
+ Sidekiq::Logging.logger = log
182
+ end
183
+
184
+ # How frequently Redis should be checked by a random Sidekiq process for
185
+ # scheduled and retriable jobs. Each individual process will take turns by
186
+ # waiting some multiple of this value.
187
+ #
188
+ # See sidekiq/scheduled.rb for an in-depth explanation of this value
189
+ def self.average_scheduled_poll_interval=(interval)
190
+ self.options[:average_scheduled_poll_interval] = interval
191
+ end
192
+
193
+ # Register a proc to handle any error which occurs within the Sidekiq process.
194
+ #
195
+ # Sidekiq.configure_server do |config|
196
+ # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
197
+ # end
198
+ #
199
+ # The default error handler logs errors to Sidekiq.logger.
200
+ def self.error_handlers
201
+ self.options[:error_handlers]
202
+ end
203
+
204
+ # Register a block to run at a point in the Sidekiq lifecycle.
205
+ # :startup, :quiet or :shutdown are valid events.
206
+ #
207
+ # Sidekiq.configure_server do |config|
208
+ # config.on(:shutdown) do
209
+ # puts "Goodbye cruel world!"
210
+ # end
211
+ # end
212
+ def self.on(event, &block)
213
+ raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
214
+ raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
215
+ options[:lifecycle_events][event] << block
216
+ end
217
+
218
+ # We are shutting down Sidekiq but what about workers that
219
+ # are working on some long job? This error is
220
+ # raised in workers that have not finished within the hard
221
+ # timeout limit. This is needed to rollback db transactions,
222
+ # otherwise Ruby's Thread#kill will commit. See #377.
223
+ # DO NOT RESCUE THIS ERROR IN YOUR WORKERS
224
+ class Shutdown < Interrupt; end
225
+
226
+ end
227
+
228
+ require 'sidekiq/rails' if defined?(::Rails::Engine)
@@ -0,0 +1,871 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ require 'sidekiq'
4
+
5
+ module Sidekiq
6
+ class Stats
7
+ def initialize
8
+ fetch_stats!
9
+ end
10
+
11
+ def processed
12
+ stat :processed
13
+ end
14
+
15
+ def failed
16
+ stat :failed
17
+ end
18
+
19
+ def scheduled_size
20
+ stat :scheduled_size
21
+ end
22
+
23
+ def retry_size
24
+ stat :retry_size
25
+ end
26
+
27
+ def dead_size
28
+ stat :dead_size
29
+ end
30
+
31
+ def enqueued
32
+ stat :enqueued
33
+ end
34
+
35
+ def processes_size
36
+ stat :processes_size
37
+ end
38
+
39
+ def workers_size
40
+ stat :workers_size
41
+ end
42
+
43
+ def default_queue_latency
44
+ stat :default_queue_latency
45
+ end
46
+
47
+ def queues
48
+ Sidekiq::Stats::Queues.new.lengths
49
+ end
50
+
51
+ def fetch_stats!
52
+ pipe1_res = Sidekiq.redis do |conn|
53
+ conn.pipelined do
54
+ conn.get('stat:processed'.freeze)
55
+ conn.get('stat:failed'.freeze)
56
+ conn.zcard('schedule'.freeze)
57
+ conn.zcard('retry'.freeze)
58
+ conn.zcard('dead'.freeze)
59
+ conn.scard('processes'.freeze)
60
+ conn.lrange('queue:default'.freeze, -1, -1)
61
+ conn.smembers('processes'.freeze)
62
+ conn.smembers('queues'.freeze)
63
+ end
64
+ end
65
+
66
+ pipe2_res = Sidekiq.redis do |conn|
67
+ conn.pipelined do
68
+ pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
69
+ pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
70
+ end
71
+ end
72
+
73
+ s = pipe1_res[7].size
74
+ workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
75
+ enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
76
+
77
+ default_queue_latency = if (entry = pipe1_res[6].first)
78
+ job = Sidekiq.load_json(entry) rescue {}
79
+ now = Time.now.to_f
80
+ thence = job['enqueued_at'.freeze] || now
81
+ now - thence
82
+ else
83
+ 0
84
+ end
85
+ @stats = {
86
+ processed: pipe1_res[0].to_i,
87
+ failed: pipe1_res[1].to_i,
88
+ scheduled_size: pipe1_res[2],
89
+ retry_size: pipe1_res[3],
90
+ dead_size: pipe1_res[4],
91
+ processes_size: pipe1_res[5],
92
+
93
+ default_queue_latency: default_queue_latency,
94
+ workers_size: workers_size,
95
+ enqueued: enqueued
96
+ }
97
+ end
98
+
99
+ def reset(*stats)
100
+ all = %w(failed processed)
101
+ stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
102
+
103
+ mset_args = []
104
+ stats.each do |stat|
105
+ mset_args << "stat:#{stat}"
106
+ mset_args << 0
107
+ end
108
+ Sidekiq.redis do |conn|
109
+ conn.mset(*mset_args)
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def stat(s)
116
+ @stats[s]
117
+ end
118
+
119
+ class Queues
120
+ def lengths
121
+ Sidekiq.redis do |conn|
122
+ queues = conn.smembers('queues'.freeze)
123
+
124
+ lengths = conn.pipelined do
125
+ queues.each do |queue|
126
+ conn.llen("queue:#{queue}")
127
+ end
128
+ end
129
+
130
+ i = 0
131
+ array_of_arrays = queues.inject({}) do |memo, queue|
132
+ memo[queue] = lengths[i]
133
+ i += 1
134
+ memo
135
+ end.sort_by { |_, size| size }
136
+
137
+ Hash[array_of_arrays.reverse]
138
+ end
139
+ end
140
+ end
141
+
142
+ class History
143
+ def initialize(days_previous, start_date = nil)
144
+ @days_previous = days_previous
145
+ @start_date = start_date || Time.now.utc.to_date
146
+ end
147
+
148
+ def processed
149
+ date_stat_hash("processed")
150
+ end
151
+
152
+ def failed
153
+ date_stat_hash("failed")
154
+ end
155
+
156
+ private
157
+
158
+ def date_stat_hash(stat)
159
+ i = 0
160
+ stat_hash = {}
161
+ keys = []
162
+ dates = []
163
+
164
+ while i < @days_previous
165
+ date = @start_date - i
166
+ datestr = date.strftime("%Y-%m-%d".freeze)
167
+ keys << "stat:#{stat}:#{datestr}"
168
+ dates << datestr
169
+ i += 1
170
+ end
171
+
172
+ Sidekiq.redis do |conn|
173
+ conn.mget(keys).each_with_index do |value, idx|
174
+ stat_hash[dates[idx]] = value ? value.to_i : 0
175
+ end
176
+ end
177
+
178
+ stat_hash
179
+ end
180
+ end
181
+ end
182
+
183
+ ##
184
+ # Encapsulates a queue within Sidekiq.
185
+ # Allows enumeration of all jobs within the queue
186
+ # and deletion of jobs.
187
+ #
188
+ # queue = Sidekiq::Queue.new("mailer")
189
+ # queue.each do |job|
190
+ # job.klass # => 'MyWorker'
191
+ # job.args # => [1, 2, 3]
192
+ # job.delete if job.jid == 'abcdef1234567890'
193
+ # end
194
+ #
195
+ class Queue
196
+ include Enumerable
197
+
198
+ ##
199
+ # Return all known queues within Redis.
200
+ #
201
+ def self.all
202
+ Sidekiq.redis { |c| c.smembers('queues'.freeze) }.sort.map { |q| Sidekiq::Queue.new(q) }
203
+ end
204
+
205
+ attr_reader :name
206
+
207
+ def initialize(name="default")
208
+ @name = name
209
+ @rname = "queue:#{name}"
210
+ end
211
+
212
+ def size
213
+ Sidekiq.redis { |con| con.llen(@rname) }
214
+ end
215
+
216
+ # Sidekiq Pro overrides this
217
+ def paused?
218
+ false
219
+ end
220
+
221
+ ##
222
+ # Calculates this queue's latency, the difference in seconds since the oldest
223
+ # job in the queue was enqueued.
224
+ #
225
+ # @return Float
226
+ def latency
227
+ entry = Sidekiq.redis do |conn|
228
+ conn.lrange(@rname, -1, -1)
229
+ end.first
230
+ return 0 unless entry
231
+ job = Sidekiq.load_json(entry)
232
+ now = Time.now.to_f
233
+ thence = job['enqueued_at'] || now
234
+ now - thence
235
+ end
236
+
237
+ def each
238
+ initial_size = size
239
+ deleted_size = 0
240
+ page = 0
241
+ page_size = 50
242
+
243
+ while true do
244
+ range_start = page * page_size - deleted_size
245
+ range_end = range_start + page_size - 1
246
+ entries = Sidekiq.redis do |conn|
247
+ conn.lrange @rname, range_start, range_end
248
+ end
249
+ break if entries.empty?
250
+ page += 1
251
+ entries.each do |entry|
252
+ yield Job.new(entry, @name)
253
+ end
254
+ deleted_size = initial_size - size
255
+ end
256
+ end
257
+
258
+ ##
259
+ # Find the job with the given JID within this queue.
260
+ #
261
+ # This is a slow, inefficient operation. Do not use under
262
+ # normal conditions. Sidekiq Pro contains a faster version.
263
+ def find_job(jid)
264
+ detect { |j| j.jid == jid }
265
+ end
266
+
267
+ def clear
268
+ Sidekiq.redis do |conn|
269
+ conn.multi do
270
+ conn.del(@rname)
271
+ conn.srem("queues".freeze, name)
272
+ end
273
+ end
274
+ end
275
+ alias_method :💣, :clear
276
+ end
277
+
278
+ ##
279
+ # Encapsulates a pending job within a Sidekiq queue or
280
+ # sorted set.
281
+ #
282
+ # The job should be considered immutable but may be
283
+ # removed from the queue via Job#delete.
284
+ #
285
+ class Job
286
+ attr_reader :item
287
+ attr_reader :value
288
+
289
+ def initialize(item, queue_name=nil)
290
+ @args = nil
291
+ @value = item
292
+ @item = item.is_a?(Hash) ? item : parse(item)
293
+ @queue = queue_name || @item['queue']
294
+ end
295
+
296
+ def parse(item)
297
+ Sidekiq.load_json(item)
298
+ rescue JSON::ParserError
299
+ # If the job payload in Redis is invalid JSON, we'll load
300
+ # the item as an empty hash and store the invalid JSON as
301
+ # the job 'args' for display in the Web UI.
302
+ @invalid = true
303
+ @args = [item]
304
+ {}
305
+ end
306
+
307
+ def klass
308
+ self['class']
309
+ end
310
+
311
+ def display_class
312
+ # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
313
+ @klass ||= case klass
314
+ when /\ASidekiq::Extensions::Delayed/
315
+ safe_load(args[0], klass) do |target, method, _|
316
+ "#{target}.#{method}"
317
+ end
318
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
319
+ job_class = @item['wrapped'] || args[0]
320
+ if 'ActionMailer::DeliveryJob' == job_class
321
+ # MailerClass#mailer_method
322
+ args[0]['arguments'][0..1].join('#')
323
+ else
324
+ job_class
325
+ end
326
+ else
327
+ klass
328
+ end
329
+ end
330
+
331
+ def display_args
332
+ # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
333
+ @args ||= case klass
334
+ when /\ASidekiq::Extensions::Delayed/
335
+ safe_load(args[0], args) do |_, _, arg|
336
+ arg
337
+ end
338
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
339
+ job_args = self['wrapped'] ? args[0]["arguments"] : []
340
+ if 'ActionMailer::DeliveryJob' == (self['wrapped'] || args[0])
341
+ # remove MailerClass, mailer_method and 'deliver_now'
342
+ job_args.drop(3)
343
+ else
344
+ job_args
345
+ end
346
+ else
347
+ if self['encrypt'.freeze]
348
+ # no point in showing 150+ bytes of random garbage
349
+ args[-1] = '[encrypted data]'.freeze
350
+ end
351
+ args
352
+ end
353
+ end
354
+
355
+ def args
356
+ @args || @item['args']
357
+ end
358
+
359
+ def jid
360
+ self['jid']
361
+ end
362
+
363
+ def enqueued_at
364
+ self['enqueued_at'] ? Time.at(self['enqueued_at']).utc : nil
365
+ end
366
+
367
+ def created_at
368
+ Time.at(self['created_at'] || self['enqueued_at'] || 0).utc
369
+ end
370
+
371
+ def queue
372
+ @queue
373
+ end
374
+
375
+ def latency
376
+ now = Time.now.to_f
377
+ now - (@item['enqueued_at'] || @item['created_at'] || now)
378
+ end
379
+
380
+ ##
381
+ # Remove this job from the queue.
382
+ def delete
383
+ count = Sidekiq.redis do |conn|
384
+ conn.lrem("queue:#{@queue}", 1, @value)
385
+ end
386
+ count != 0
387
+ end
388
+
389
+ def [](name)
390
+ # nil will happen if the JSON fails to parse.
391
+ # We don't guarantee Sidekiq will work with bad job JSON but we should
392
+ # make a best effort to minimize the damage.
393
+ @item ? @item[name] : nil
394
+ end
395
+
396
+ private
397
+
398
+ def safe_load(content, default)
399
+ begin
400
+ yield(*YAML.load(content))
401
+ rescue => ex
402
+ # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
403
+ # memory yet so the YAML can't be loaded.
404
+ Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == 'development'
405
+ default
406
+ end
407
+ end
408
+ end
409
+
410
+ class SortedEntry < Job
411
+ attr_reader :score
412
+ attr_reader :parent
413
+
414
+ def initialize(parent, score, item)
415
+ super(item)
416
+ @score = score
417
+ @parent = parent
418
+ end
419
+
420
+ def at
421
+ Time.at(score).utc
422
+ end
423
+
424
+ def delete
425
+ if @value
426
+ @parent.delete_by_value(@parent.name, @value)
427
+ else
428
+ @parent.delete_by_jid(score, jid)
429
+ end
430
+ end
431
+
432
+ def reschedule(at)
433
+ delete
434
+ @parent.schedule(at, item)
435
+ end
436
+
437
+ def add_to_queue
438
+ remove_job do |message|
439
+ msg = Sidekiq.load_json(message)
440
+ Sidekiq::Client.push(msg)
441
+ end
442
+ end
443
+
444
+ def retry
445
+ remove_job do |message|
446
+ msg = Sidekiq.load_json(message)
447
+ msg['retry_count'] -= 1 if msg['retry_count']
448
+ Sidekiq::Client.push(msg)
449
+ end
450
+ end
451
+
452
+ ##
453
+ # Place job in the dead set
454
+ def kill
455
+ remove_job do |message|
456
+ now = Time.now.to_f
457
+ Sidekiq.redis do |conn|
458
+ conn.multi do
459
+ conn.zadd('dead', now, message)
460
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
461
+ conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
462
+ end
463
+ end
464
+ end
465
+ end
466
+
467
+ def error?
468
+ !!item['error_class']
469
+ end
470
+
471
+ private
472
+
473
+ def remove_job
474
+ Sidekiq.redis do |conn|
475
+ results = conn.multi do
476
+ conn.zrangebyscore(parent.name, score, score)
477
+ conn.zremrangebyscore(parent.name, score, score)
478
+ end.first
479
+
480
+ if results.size == 1
481
+ yield results.first
482
+ else
483
+ # multiple jobs with the same score
484
+ # find the one with the right JID and push it
485
+ hash = results.group_by do |message|
486
+ if message.index(jid)
487
+ msg = Sidekiq.load_json(message)
488
+ msg['jid'] == jid
489
+ else
490
+ false
491
+ end
492
+ end
493
+
494
+ msg = hash.fetch(true, []).first
495
+ yield msg if msg
496
+
497
+ # push the rest back onto the sorted set
498
+ conn.multi do
499
+ hash.fetch(false, []).each do |message|
500
+ conn.zadd(parent.name, score.to_f.to_s, message)
501
+ end
502
+ end
503
+ end
504
+ end
505
+ end
506
+
507
+ end
508
+
509
+ class SortedSet
510
+ include Enumerable
511
+
512
+ attr_reader :name
513
+
514
+ def initialize(name)
515
+ @name = name
516
+ @_size = size
517
+ end
518
+
519
+ def size
520
+ Sidekiq.redis { |c| c.zcard(name) }
521
+ end
522
+
523
+ def clear
524
+ Sidekiq.redis do |conn|
525
+ conn.del(name)
526
+ end
527
+ end
528
+ alias_method :💣, :clear
529
+ end
530
+
531
+ class JobSet < SortedSet
532
+
533
+ def schedule(timestamp, message)
534
+ Sidekiq.redis do |conn|
535
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
536
+ end
537
+ end
538
+
539
+ def each
540
+ initial_size = @_size
541
+ offset_size = 0
542
+ page = -1
543
+ page_size = 50
544
+
545
+ while true do
546
+ range_start = page * page_size + offset_size
547
+ range_end = range_start + page_size - 1
548
+ elements = Sidekiq.redis do |conn|
549
+ conn.zrange name, range_start, range_end, with_scores: true
550
+ end
551
+ break if elements.empty?
552
+ page -= 1
553
+ elements.each do |element, score|
554
+ yield SortedEntry.new(self, score, element)
555
+ end
556
+ offset_size = initial_size - @_size
557
+ end
558
+ end
559
+
560
+ def fetch(score, jid = nil)
561
+ elements = Sidekiq.redis do |conn|
562
+ conn.zrangebyscore(name, score, score)
563
+ end
564
+
565
+ elements.inject([]) do |result, element|
566
+ entry = SortedEntry.new(self, score, element)
567
+ if jid
568
+ result << entry if entry.jid == jid
569
+ else
570
+ result << entry
571
+ end
572
+ result
573
+ end
574
+ end
575
+
576
+ ##
577
+ # Find the job with the given JID within this sorted set.
578
+ #
579
+ # This is a slow, inefficient operation. Do not use under
580
+ # normal conditions. Sidekiq Pro contains a faster version.
581
+ def find_job(jid)
582
+ self.detect { |j| j.jid == jid }
583
+ end
584
+
585
+ def delete_by_value(name, value)
586
+ Sidekiq.redis do |conn|
587
+ ret = conn.zrem(name, value)
588
+ @_size -= 1 if ret
589
+ ret
590
+ end
591
+ end
592
+
593
+ def delete_by_jid(score, jid)
594
+ Sidekiq.redis do |conn|
595
+ elements = conn.zrangebyscore(name, score, score)
596
+ elements.each do |element|
597
+ message = Sidekiq.load_json(element)
598
+ if message["jid"] == jid
599
+ ret = conn.zrem(name, element)
600
+ @_size -= 1 if ret
601
+ break ret
602
+ end
603
+ false
604
+ end
605
+ end
606
+ end
607
+
608
+ alias_method :delete, :delete_by_jid
609
+ end
610
+
611
+ ##
612
+ # Allows enumeration of scheduled jobs within Sidekiq.
613
+ # Based on this, you can search/filter for jobs. Here's an
614
+ # example where I'm selecting all jobs of a certain type
615
+ # and deleting them from the schedule queue.
616
+ #
617
+ # r = Sidekiq::ScheduledSet.new
618
+ # r.select do |scheduled|
619
+ # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
620
+ # scheduled.args[0] == 'User' &&
621
+ # scheduled.args[1] == 'setup_new_subscriber'
622
+ # end.map(&:delete)
623
+ class ScheduledSet < JobSet
624
+ def initialize
625
+ super 'schedule'
626
+ end
627
+ end
628
+
629
+ ##
630
+ # Allows enumeration of retries within Sidekiq.
631
+ # Based on this, you can search/filter for jobs. Here's an
632
+ # example where I'm selecting all jobs of a certain type
633
+ # and deleting them from the retry queue.
634
+ #
635
+ # r = Sidekiq::RetrySet.new
636
+ # r.select do |retri|
637
+ # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
638
+ # retri.args[0] == 'User' &&
639
+ # retri.args[1] == 'setup_new_subscriber'
640
+ # end.map(&:delete)
641
+ class RetrySet < JobSet
642
+ def initialize
643
+ super 'retry'
644
+ end
645
+
646
+ def retry_all
647
+ while size > 0
648
+ each(&:retry)
649
+ end
650
+ end
651
+ end
652
+
653
+ ##
654
+ # Allows enumeration of dead jobs within Sidekiq.
655
+ #
656
+ class DeadSet < JobSet
657
+ def initialize
658
+ super 'dead'
659
+ end
660
+
661
+ def retry_all
662
+ while size > 0
663
+ each(&:retry)
664
+ end
665
+ end
666
+
667
+ def self.max_jobs
668
+ Sidekiq.options[:dead_max_jobs]
669
+ end
670
+
671
+ def self.timeout
672
+ Sidekiq.options[:dead_timeout_in_seconds]
673
+ end
674
+ end
675
+
676
+ ##
677
+ # Enumerates the set of Sidekiq processes which are actively working
678
+ # right now. Each process send a heartbeat to Redis every 5 seconds
679
+ # so this set should be relatively accurate, barring network partitions.
680
+ #
681
+ # Yields a Sidekiq::Process.
682
+ #
683
+ class ProcessSet
684
+ include Enumerable
685
+
686
+ def initialize(clean_plz=true)
687
+ self.class.cleanup if clean_plz
688
+ end
689
+
690
+ # Cleans up dead processes recorded in Redis.
691
+ # Returns the number of processes cleaned.
692
+ def self.cleanup
693
+ count = 0
694
+ Sidekiq.redis do |conn|
695
+ procs = conn.smembers('processes').sort
696
+ heartbeats = conn.pipelined do
697
+ procs.each do |key|
698
+ conn.hget(key, 'info')
699
+ end
700
+ end
701
+
702
+ # the hash named key has an expiry of 60 seconds.
703
+ # if it's not found, that means the process has not reported
704
+ # in to Redis and probably died.
705
+ to_prune = []
706
+ heartbeats.each_with_index do |beat, i|
707
+ to_prune << procs[i] if beat.nil?
708
+ end
709
+ count = conn.srem('processes', to_prune) unless to_prune.empty?
710
+ end
711
+ count
712
+ end
713
+
714
+ def each
715
+ procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
716
+
717
+ Sidekiq.redis do |conn|
718
+ # We're making a tradeoff here between consuming more memory instead of
719
+ # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
720
+ # you'll be happier this way
721
+ result = conn.pipelined do
722
+ procs.each do |key|
723
+ conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
724
+ end
725
+ end
726
+
727
+ result.each do |info, busy, at_s, quiet|
728
+ hash = Sidekiq.load_json(info)
729
+ yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
730
+ end
731
+ end
732
+
733
+ nil
734
+ end
735
+
736
+ # This method is not guaranteed accurate since it does not prune the set
737
+ # based on current heartbeat. #each does that and ensures the set only
738
+ # contains Sidekiq processes which have sent a heartbeat within the last
739
+ # 60 seconds.
740
+ def size
741
+ Sidekiq.redis { |conn| conn.scard('processes') }
742
+ end
743
+ end
744
+
745
+ #
746
+ # Sidekiq::Process represents an active Sidekiq process talking with Redis.
747
+ # Each process has a set of attributes which look like this:
748
+ #
749
+ # {
750
+ # 'hostname' => 'app-1.example.com',
751
+ # 'started_at' => <process start time>,
752
+ # 'pid' => 12345,
753
+ # 'tag' => 'myapp'
754
+ # 'concurrency' => 25,
755
+ # 'queues' => ['default', 'low'],
756
+ # 'busy' => 10,
757
+ # 'beat' => <last heartbeat>,
758
+ # 'identity' => <unique string identifying the process>,
759
+ # }
760
+ class Process
761
+ def initialize(hash)
762
+ @attribs = hash
763
+ end
764
+
765
+ def tag
766
+ self['tag']
767
+ end
768
+
769
+ def labels
770
+ Array(self['labels'])
771
+ end
772
+
773
+ def [](key)
774
+ @attribs[key]
775
+ end
776
+
777
+ def quiet!
778
+ signal('TSTP')
779
+ end
780
+
781
+ def stop!
782
+ signal('TERM')
783
+ end
784
+
785
+ def dump_threads
786
+ signal('TTIN')
787
+ end
788
+
789
+ def stopping?
790
+ self['quiet'] == 'true'
791
+ end
792
+
793
+ private
794
+
795
+ def signal(sig)
796
+ key = "#{identity}-signals"
797
+ Sidekiq.redis do |c|
798
+ c.multi do
799
+ c.lpush(key, sig)
800
+ c.expire(key, 60)
801
+ end
802
+ end
803
+ end
804
+
805
+ def identity
806
+ self['identity']
807
+ end
808
+ end
809
+
810
+ ##
811
+ # A worker is a thread that is currently processing a job.
812
+ # Programmatic access to the current active worker set.
813
+ #
814
+ # WARNING WARNING WARNING
815
+ #
816
+ # This is live data that can change every millisecond.
817
+ # If you call #size => 5 and then expect #each to be
818
+ # called 5 times, you're going to have a bad time.
819
+ #
820
+ # workers = Sidekiq::Workers.new
821
+ # workers.size => 2
822
+ # workers.each do |process_id, thread_id, work|
823
+ # # process_id is a unique identifier per Sidekiq process
824
+ # # thread_id is a unique identifier per thread
825
+ # # work is a Hash which looks like:
826
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
827
+ # # run_at is an epoch Integer.
828
+ # end
829
+ #
830
+ class Workers
831
+ include Enumerable
832
+
833
+ def each
834
+ Sidekiq.redis do |conn|
835
+ procs = conn.smembers('processes')
836
+ procs.sort.each do |key|
837
+ valid, workers = conn.pipelined do
838
+ conn.exists(key)
839
+ conn.hgetall("#{key}:workers")
840
+ end
841
+ next unless valid
842
+ workers.each_pair do |tid, json|
843
+ yield key, tid, Sidekiq.load_json(json)
844
+ end
845
+ end
846
+ end
847
+ end
848
+
849
+ # Note that #size is only as accurate as Sidekiq's heartbeat,
850
+ # which happens every 5 seconds. It is NOT real-time.
851
+ #
852
+ # Not very efficient if you have lots of Sidekiq
853
+ # processes but the alternative is a global counter
854
+ # which can easily get out of sync with crashy processes.
855
+ def size
856
+ Sidekiq.redis do |conn|
857
+ procs = conn.smembers('processes')
858
+ if procs.empty?
859
+ 0
860
+ else
861
+ conn.pipelined do
862
+ procs.each do |key|
863
+ conn.hget(key, 'busy')
864
+ end
865
+ end.map(&:to_i).inject(:+)
866
+ end
867
+ end
868
+ end
869
+ end
870
+
871
+ end