sidekiq 6.4.2 → 6.5.2

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +35 -0
  3. data/bin/sidekiqload +15 -3
  4. data/lib/sidekiq/api.rb +160 -32
  5. data/lib/sidekiq/cli.rb +34 -32
  6. data/lib/sidekiq/client.rb +4 -4
  7. data/lib/sidekiq/component.rb +65 -0
  8. data/lib/sidekiq/delay.rb +1 -1
  9. data/lib/sidekiq/fetch.rb +16 -14
  10. data/lib/sidekiq/job_retry.rb +50 -34
  11. data/lib/sidekiq/job_util.rb +7 -3
  12. data/lib/sidekiq/launcher.rb +22 -19
  13. data/lib/sidekiq/logger.rb +1 -1
  14. data/lib/sidekiq/manager.rb +23 -20
  15. data/lib/sidekiq/metrics/deploy.rb +47 -0
  16. data/lib/sidekiq/metrics/query.rb +124 -0
  17. data/lib/sidekiq/metrics/shared.rb +94 -0
  18. data/lib/sidekiq/metrics/tracking.rb +134 -0
  19. data/lib/sidekiq/middleware/chain.rb +82 -38
  20. data/lib/sidekiq/middleware/current_attributes.rb +10 -4
  21. data/lib/sidekiq/middleware/i18n.rb +2 -0
  22. data/lib/sidekiq/middleware/modules.rb +21 -0
  23. data/lib/sidekiq/paginator.rb +2 -2
  24. data/lib/sidekiq/processor.rb +13 -13
  25. data/lib/sidekiq/rails.rb +5 -5
  26. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  27. data/lib/sidekiq/redis_connection.rb +80 -47
  28. data/lib/sidekiq/ring_buffer.rb +29 -0
  29. data/lib/sidekiq/scheduled.rb +11 -10
  30. data/lib/sidekiq/testing.rb +1 -1
  31. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  32. data/lib/sidekiq/version.rb +1 -1
  33. data/lib/sidekiq/web/application.rb +13 -0
  34. data/lib/sidekiq/web/helpers.rb +25 -2
  35. data/lib/sidekiq/web.rb +4 -0
  36. data/lib/sidekiq/worker.rb +2 -1
  37. data/lib/sidekiq.rb +87 -18
  38. data/sidekiq.gemspec +1 -1
  39. data/web/assets/javascripts/application.js +1 -1
  40. data/web/assets/javascripts/dashboard.js +0 -17
  41. data/web/assets/javascripts/graph.js +16 -0
  42. data/web/locales/en.yml +4 -0
  43. data/web/locales/pt-br.yml +27 -9
  44. data/web/views/_nav.erb +1 -1
  45. data/web/views/busy.erb +1 -1
  46. data/web/views/dashboard.erb +1 -0
  47. data/web/views/metrics.erb +59 -0
  48. data/web/views/metrics_for_job.erb +92 -0
  49. data/web/views/queue.erb +5 -1
  50. metadata +16 -6
  51. data/lib/sidekiq/exception_handler.rb +0 -27
  52. data/lib/sidekiq/util.rb +0 -108
@@ -1,82 +1,102 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sidekiq/middleware/modules"
4
+
3
5
  module Sidekiq
4
6
  # Middleware is code configured to run before/after
5
- # a message is processed. It is patterned after Rack
7
+ # a job is processed. It is patterned after Rack
6
8
  # middleware. Middleware exists for the client side
7
9
  # (pushing jobs onto the queue) as well as the server
8
10
  # side (when jobs are actually processed).
9
11
  #
12
+ # Callers will register middleware Classes and Sidekiq will
13
+ # create new instances of the middleware for every job. This
14
+ # is important so that instance state is not shared accidentally
15
+ # between job executions.
16
+ #
10
17
  # To add middleware for the client:
11
18
  #
12
- # Sidekiq.configure_client do |config|
13
- # config.client_middleware do |chain|
14
- # chain.add MyClientHook
19
+ # Sidekiq.configure_client do |config|
20
+ # config.client_middleware do |chain|
21
+ # chain.add MyClientHook
22
+ # end
15
23
  # end
16
- # end
17
24
  #
18
25
  # To modify middleware for the server, just call
19
26
  # with another block:
20
27
  #
21
- # Sidekiq.configure_server do |config|
22
- # config.server_middleware do |chain|
23
- # chain.add MyServerHook
24
- # chain.remove ActiveRecord
28
+ # Sidekiq.configure_server do |config|
29
+ # config.server_middleware do |chain|
30
+ # chain.add MyServerHook
31
+ # chain.remove ActiveRecord
32
+ # end
25
33
  # end
26
- # end
27
34
  #
28
35
  # To insert immediately preceding another entry:
29
36
  #
30
- # Sidekiq.configure_client do |config|
31
- # config.client_middleware do |chain|
32
- # chain.insert_before ActiveRecord, MyClientHook
37
+ # Sidekiq.configure_client do |config|
38
+ # config.client_middleware do |chain|
39
+ # chain.insert_before ActiveRecord, MyClientHook
40
+ # end
33
41
  # end
34
- # end
35
42
  #
36
43
  # To insert immediately after another entry:
37
44
  #
38
- # Sidekiq.configure_client do |config|
39
- # config.client_middleware do |chain|
40
- # chain.insert_after ActiveRecord, MyClientHook
45
+ # Sidekiq.configure_client do |config|
46
+ # config.client_middleware do |chain|
47
+ # chain.insert_after ActiveRecord, MyClientHook
48
+ # end
41
49
  # end
42
- # end
43
50
  #
44
51
  # This is an example of a minimal server middleware:
45
52
  #
46
- # class MyServerHook
47
- # def call(job_instance, msg, queue)
48
- # puts "Before job"
49
- # yield
50
- # puts "After job"
53
+ # class MyServerHook
54
+ # include Sidekiq::ServerMiddleware
55
+ #
56
+ # def call(job_instance, msg, queue)
57
+ # logger.info "Before job"
58
+ # redis {|conn| conn.get("foo") } # do something in Redis
59
+ # yield
60
+ # logger.info "After job"
61
+ # end
51
62
  # end
52
- # end
53
63
  #
54
64
  # This is an example of a minimal client middleware, note
55
65
  # the method must return the result or the job will not push
56
66
  # to Redis:
57
67
  #
58
- # class MyClientHook
59
- # def call(job_class, msg, queue, redis_pool)
60
- # puts "Before push"
61
- # result = yield
62
- # puts "After push"
63
- # result
68
+ # class MyClientHook
69
+ # include Sidekiq::ClientMiddleware
70
+ #
71
+ # def call(job_class, msg, queue, redis_pool)
72
+ # logger.info "Before push"
73
+ # result = yield
74
+ # logger.info "After push"
75
+ # result
76
+ # end
64
77
  # end
65
- # end
66
78
  #
67
79
  module Middleware
68
80
  class Chain
69
81
  include Enumerable
70
82
 
83
+ # A unique instance of the middleware chain is created for
84
+ # each job executed in order to be thread-safe.
85
+ # @param copy [Sidekiq::Middleware::Chain] New instance of Chain
86
+ # @returns nil
71
87
  def initialize_copy(copy)
72
88
  copy.instance_variable_set(:@entries, entries.dup)
89
+ nil
73
90
  end
74
91
 
92
+ # Iterate through each middleware in the chain
75
93
  def each(&block)
76
94
  entries.each(&block)
77
95
  end
78
96
 
79
- def initialize
97
+ # @api private
98
+ def initialize(config = nil) # :nodoc:
99
+ @config = config
80
100
  @entries = nil
81
101
  yield self if block_given?
82
102
  end
@@ -85,38 +105,55 @@ module Sidekiq
85
105
  @entries ||= []
86
106
  end
87
107
 
108
+ # Remove all middleware matching the given Class
109
+ # @param klass [Class]
88
110
  def remove(klass)
89
111
  entries.delete_if { |entry| entry.klass == klass }
90
112
  end
91
113
 
114
+ # Add the given middleware to the end of the chain.
115
+ # Sidekiq will call `klass.new(*args)` to create a clean
116
+ # copy of your middleware for every job executed.
117
+ #
118
+ # chain.add(Statsd::Metrics, { collector: "localhost:8125" })
119
+ #
120
+ # @param klass [Class] Your middleware class
121
+ # @param *args [Array<Object>] Set of arguments to pass to every instance of your middleware
92
122
  def add(klass, *args)
93
123
  remove(klass)
94
- entries << Entry.new(klass, *args)
124
+ entries << Entry.new(@config, klass, *args)
95
125
  end
96
126
 
127
+ # Identical to {#add} except the middleware is added to the front of the chain.
97
128
  def prepend(klass, *args)
98
129
  remove(klass)
99
- entries.insert(0, Entry.new(klass, *args))
130
+ entries.insert(0, Entry.new(@config, klass, *args))
100
131
  end
101
132
 
133
+ # Inserts +newklass+ before +oldklass+ in the chain.
134
+ # Useful if one middleware must run before another middleware.
102
135
  def insert_before(oldklass, newklass, *args)
103
136
  i = entries.index { |entry| entry.klass == newklass }
104
- new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
137
+ new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
105
138
  i = entries.index { |entry| entry.klass == oldklass } || 0
106
139
  entries.insert(i, new_entry)
107
140
  end
108
141
 
142
+ # Inserts +newklass+ after +oldklass+ in the chain.
143
+ # Useful if one middleware must run after another middleware.
109
144
  def insert_after(oldklass, newklass, *args)
110
145
  i = entries.index { |entry| entry.klass == newklass }
111
- new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
146
+ new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
112
147
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
113
148
  entries.insert(i + 1, new_entry)
114
149
  end
115
150
 
151
+ # @return [Boolean] if the given class is already in the chain
116
152
  def exists?(klass)
117
153
  any? { |entry| entry.klass == klass }
118
154
  end
119
155
 
156
+ # @return [Boolean] if the chain contains no middleware
120
157
  def empty?
121
158
  @entries.nil? || @entries.empty?
122
159
  end
@@ -129,6 +166,8 @@ module Sidekiq
129
166
  entries.clear
130
167
  end
131
168
 
169
+ # Used by Sidekiq to execute the middleware at runtime
170
+ # @api private
132
171
  def invoke(*args)
133
172
  return yield if empty?
134
173
 
@@ -146,16 +185,21 @@ module Sidekiq
146
185
 
147
186
  private
148
187
 
188
+ # Represents each link in the middleware chain
189
+ # @api private
149
190
  class Entry
150
191
  attr_reader :klass
151
192
 
152
- def initialize(klass, *args)
193
+ def initialize(config, klass, *args)
194
+ @config = config
153
195
  @klass = klass
154
196
  @args = args
155
197
  end
156
198
 
157
199
  def make_new
158
- @klass.new(*@args)
200
+ x = @klass.new(*@args)
201
+ x.config = @config if @config && x.respond_to?(:config=)
202
+ x
159
203
  end
160
204
  end
161
205
  end
@@ -15,22 +15,28 @@ module Sidekiq
15
15
  #
16
16
  module CurrentAttributes
17
17
  class Save
18
+ include Sidekiq::ClientMiddleware
19
+
18
20
  def initialize(cattr)
19
21
  @klass = cattr
20
22
  end
21
23
 
22
24
  def call(_, job, _, _)
23
25
  attrs = @klass.attributes
24
- if job.has_key?("cattr")
25
- job["cattr"].merge!(attrs)
26
- else
27
- job["cattr"] = attrs
26
+ if attrs.any?
27
+ if job.has_key?("cattr")
28
+ job["cattr"].merge!(attrs)
29
+ else
30
+ job["cattr"] = attrs
31
+ end
28
32
  end
29
33
  yield
30
34
  end
31
35
  end
32
36
 
33
37
  class Load
38
+ include Sidekiq::ServerMiddleware
39
+
34
40
  def initialize(cattr)
35
41
  @klass = cattr
36
42
  end
@@ -10,6 +10,7 @@ module Sidekiq::Middleware::I18n
10
10
  # Get the current locale and store it in the message
11
11
  # to be sent to Sidekiq.
12
12
  class Client
13
+ include Sidekiq::ClientMiddleware
13
14
  def call(_jobclass, job, _queue, _redis)
14
15
  job["locale"] ||= I18n.locale
15
16
  yield
@@ -18,6 +19,7 @@ module Sidekiq::Middleware::I18n
18
19
 
19
20
  # Pull the msg locale out and set the current thread to use it.
20
21
  class Server
22
+ include Sidekiq::ServerMiddleware
21
23
  def call(_jobclass, job, _queue, &block)
22
24
  I18n.with_locale(job.fetch("locale", I18n.default_locale), &block)
23
25
  end
@@ -0,0 +1,21 @@
1
+ module Sidekiq
2
+ # Server-side middleware must import this Module in order
3
+ # to get access to server resources during `call`.
4
+ module ServerMiddleware
5
+ attr_accessor :config
6
+ def redis_pool
7
+ config.redis_pool
8
+ end
9
+
10
+ def logger
11
+ config.logger
12
+ end
13
+
14
+ def redis(&block)
15
+ config.redis(&block)
16
+ end
17
+ end
18
+
19
+ # no difference for now
20
+ ClientMiddleware = ServerMiddleware
21
+ end
@@ -19,9 +19,9 @@ module Sidekiq
19
19
  total_size, items = conn.multi { |transaction|
20
20
  transaction.zcard(key)
21
21
  if rev
22
- transaction.zrevrange(key, starting, ending, with_scores: true)
22
+ transaction.zrevrange(key, starting, ending, withscores: true)
23
23
  else
24
- transaction.zrange(key, starting, ending, with_scores: true)
24
+ transaction.zrange(key, starting, ending, withscores: true)
25
25
  end
26
26
  }
27
27
  [current_page, total_size, items]
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/util"
4
3
  require "sidekiq/fetch"
5
4
  require "sidekiq/job_logger"
6
5
  require "sidekiq/job_retry"
@@ -15,29 +14,30 @@ module Sidekiq
15
14
  # b. run the middleware chain
16
15
  # c. call #perform
17
16
  #
18
- # A Processor can exit due to shutdown (processor_stopped)
19
- # or due to an error during job execution (processor_died)
17
+ # A Processor can exit due to shutdown or due to
18
+ # an error during job execution.
20
19
  #
21
20
  # If an error occurs in the job execution, the
22
21
  # Processor calls the Manager to create a new one
23
22
  # to replace itself and exits.
24
23
  #
25
24
  class Processor
26
- include Util
25
+ include Sidekiq::Component
27
26
 
28
27
  attr_reader :thread
29
28
  attr_reader :job
30
29
 
31
- def initialize(mgr, options)
32
- @mgr = mgr
30
+ def initialize(options, &block)
31
+ @callback = block
33
32
  @down = false
34
33
  @done = false
35
34
  @job = nil
36
35
  @thread = nil
36
+ @config = options
37
37
  @strategy = options[:fetch]
38
38
  @reloader = options[:reloader] || proc { |&block| block.call }
39
39
  @job_logger = (options[:job_logger] || Sidekiq::JobLogger).new
40
- @retrier = Sidekiq::JobRetry.new
40
+ @retrier = Sidekiq::JobRetry.new(options)
41
41
  end
42
42
 
43
43
  def terminate(wait = false)
@@ -66,14 +66,14 @@ module Sidekiq
66
66
 
67
67
  def run
68
68
  process_one until @done
69
- @mgr.processor_stopped(self)
69
+ @callback.call(self)
70
70
  rescue Sidekiq::Shutdown
71
- @mgr.processor_stopped(self)
71
+ @callback.call(self)
72
72
  rescue Exception => ex
73
- @mgr.processor_died(self, ex)
73
+ @callback.call(self, ex)
74
74
  end
75
75
 
76
- def process_one
76
+ def process_one(&block)
77
77
  @job = fetch
78
78
  process(@job) if @job
79
79
  @job = nil
@@ -160,7 +160,7 @@ module Sidekiq
160
160
  ack = false
161
161
  begin
162
162
  dispatch(job_hash, queue, jobstr) do |inst|
163
- Sidekiq.server_middleware.invoke(inst, job_hash, queue) do
163
+ @config.server_middleware.invoke(inst, job_hash, queue) do
164
164
  execute_job(inst, job_hash["args"])
165
165
  end
166
166
  end
@@ -174,7 +174,7 @@ module Sidekiq
174
174
  # signals that we created a retry successfully. We can acknowlege the job.
175
175
  ack = true
176
176
  e = h.cause || h
177
- handle_exception(e, {context: "Job raised exception", job: job_hash, jobstr: jobstr})
177
+ handle_exception(e, {context: "Job raised exception", job: job_hash})
178
178
  raise e
179
179
  rescue Exception => ex
180
180
  # Unexpected error! This is very bad and indicates an exception that got past
data/lib/sidekiq/rails.rb CHANGED
@@ -38,12 +38,12 @@ module Sidekiq
38
38
  end
39
39
 
40
40
  initializer "sidekiq.rails_logger" do
41
- Sidekiq.configure_server do |_|
41
+ Sidekiq.configure_server do |config|
42
42
  # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
43
43
  # it will appear in the Sidekiq console with all of the job context. See #5021 and
44
44
  # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
45
- unless ::Rails.logger == ::Sidekiq.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
46
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(::Sidekiq.logger))
45
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
46
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
47
47
  end
48
48
  end
49
49
  end
@@ -60,8 +60,8 @@ module Sidekiq
60
60
  #
61
61
  # None of this matters on the client-side, only within the Sidekiq process itself.
62
62
  config.after_initialize do
63
- Sidekiq.configure_server do |_|
64
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
63
+ Sidekiq.configure_server do |config|
64
+ config[:reloader] = Sidekiq::Rails::Reloader.new
65
65
  end
66
66
  end
67
67
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "connection_pool"
4
+ require "redis_client"
5
+ require "redis_client/decorator"
6
+ require "uri"
7
+
8
+ module Sidekiq
9
+ class RedisClientAdapter
10
+ BaseError = RedisClient::Error
11
+ CommandError = RedisClient::CommandError
12
+
13
+ module CompatMethods
14
+ def info
15
+ @client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
16
+ end
17
+
18
+ def evalsha(sha, keys, argv)
19
+ @client.call("EVALSHA", sha, keys.size, *keys, *argv)
20
+ end
21
+
22
+ def brpoplpush(*args)
23
+ @client.blocking_call(false, "BRPOPLPUSH", *args)
24
+ end
25
+
26
+ def brpop(*args)
27
+ @client.blocking_call(false, "BRPOP", *args)
28
+ end
29
+
30
+ def set(*args)
31
+ @client.call("SET", *args) { |r| r == "OK" }
32
+ end
33
+ ruby2_keywords :set if respond_to?(:ruby2_keywords, true)
34
+
35
+ def sismember(*args)
36
+ @client.call("SISMEMBER", *args) { |c| c > 0 }
37
+ end
38
+
39
+ def exists?(key)
40
+ @client.call("EXISTS", key) { |c| c > 0 }
41
+ end
42
+
43
+ private
44
+
45
+ def method_missing(*args, &block)
46
+ @client.call(*args, *block)
47
+ end
48
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
49
+
50
+ def respond_to_missing?(name, include_private = false)
51
+ super # Appease the linter. We can't tell what is a valid command.
52
+ end
53
+ end
54
+
55
+ CompatClient = RedisClient::Decorator.create(CompatMethods)
56
+
57
+ class CompatClient
58
+ %i[scan sscan zscan hscan].each do |method|
59
+ alias_method :"#{method}_each", method
60
+ undef_method method
61
+ end
62
+
63
+ def disconnect!
64
+ @client.close
65
+ end
66
+
67
+ def connection
68
+ {id: @client.id}
69
+ end
70
+
71
+ def redis
72
+ self
73
+ end
74
+
75
+ def _client
76
+ @client
77
+ end
78
+
79
+ def message
80
+ yield nil, @queue.pop
81
+ end
82
+
83
+ # NB: this method does not return
84
+ def subscribe(chan)
85
+ @queue = ::Queue.new
86
+
87
+ pubsub = @client.pubsub
88
+ pubsub.call("subscribe", chan)
89
+
90
+ loop do
91
+ evt = pubsub.next_event
92
+ next if evt.nil?
93
+ next unless evt[0] == "message" && evt[1] == chan
94
+
95
+ (_, _, msg) = evt
96
+ @queue << msg
97
+ yield self
98
+ end
99
+ end
100
+ end
101
+
102
+ def initialize(options)
103
+ opts = client_opts(options)
104
+ @config = if opts.key?(:sentinels)
105
+ RedisClient.sentinel(**opts)
106
+ else
107
+ RedisClient.config(**opts)
108
+ end
109
+ end
110
+
111
+ def new_client
112
+ CompatClient.new(@config.new_client)
113
+ end
114
+
115
+ private
116
+
117
+ def client_opts(options)
118
+ opts = options.dup
119
+
120
+ if opts[:namespace]
121
+ Sidekiq.logger.error("Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature isn't supported by redis-client. " \
122
+ "Either use the redis adapter or remove the namespace.")
123
+ Kernel.exit(-127)
124
+ end
125
+
126
+ opts.delete(:size)
127
+ opts.delete(:pool_timeout)
128
+
129
+ if opts[:network_timeout]
130
+ opts[:timeout] = opts[:network_timeout]
131
+ opts.delete(:network_timeout)
132
+ end
133
+
134
+ if opts[:driver]
135
+ opts[:driver] = opts[:driver].to_sym
136
+ end
137
+
138
+ opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
139
+ opts[:role] = opts[:role].to_sym if opts.key?(:role)
140
+ opts.delete(:url) if opts.key?(:sentinels)
141
+
142
+ # Issue #3303, redis-rb will silently retry an operation.
143
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
144
+ # is performed twice but I believe this is much, much rarer
145
+ # than the reconnect silently fixing a problem; we keep it
146
+ # on by default.
147
+ opts[:reconnect_attempts] ||= 1
148
+
149
+ opts
150
+ end
151
+ end
152
+ end
153
+
154
+ Sidekiq::RedisConnection.adapter = Sidekiq::RedisClientAdapter