sidekiq 5.2.7 → 6.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -2
  3. data/.standard.yml +20 -0
  4. data/6.0-Upgrade.md +70 -0
  5. data/Changes.md +34 -0
  6. data/Ent-2.0-Upgrade.md +37 -0
  7. data/Ent-Changes.md +12 -0
  8. data/Gemfile +12 -11
  9. data/Gemfile.lock +196 -0
  10. data/Pro-5.0-Upgrade.md +25 -0
  11. data/Pro-Changes.md +12 -3
  12. data/README.md +16 -30
  13. data/Rakefile +5 -4
  14. data/bin/sidekiqload +26 -22
  15. data/bin/sidekiqmon +9 -0
  16. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  17. data/lib/generators/sidekiq/worker_generator.rb +12 -14
  18. data/lib/sidekiq.rb +53 -42
  19. data/lib/sidekiq/api.rb +138 -151
  20. data/lib/sidekiq/cli.rb +97 -162
  21. data/lib/sidekiq/client.rb +45 -46
  22. data/lib/sidekiq/delay.rb +5 -6
  23. data/lib/sidekiq/exception_handler.rb +10 -12
  24. data/lib/sidekiq/extensions/action_mailer.rb +10 -20
  25. data/lib/sidekiq/extensions/active_record.rb +9 -7
  26. data/lib/sidekiq/extensions/class_methods.rb +9 -7
  27. data/lib/sidekiq/extensions/generic_proxy.rb +4 -4
  28. data/lib/sidekiq/fetch.rb +5 -6
  29. data/lib/sidekiq/job_logger.rb +37 -7
  30. data/lib/sidekiq/job_retry.rb +45 -58
  31. data/lib/sidekiq/launcher.rb +59 -51
  32. data/lib/sidekiq/logger.rb +69 -0
  33. data/lib/sidekiq/manager.rb +7 -9
  34. data/lib/sidekiq/middleware/chain.rb +3 -2
  35. data/lib/sidekiq/middleware/i18n.rb +5 -7
  36. data/lib/sidekiq/monitor.rb +148 -0
  37. data/lib/sidekiq/paginator.rb +11 -12
  38. data/lib/sidekiq/processor.rb +52 -49
  39. data/lib/sidekiq/rails.rb +23 -29
  40. data/lib/sidekiq/redis_connection.rb +31 -37
  41. data/lib/sidekiq/scheduled.rb +17 -19
  42. data/lib/sidekiq/testing.rb +22 -23
  43. data/lib/sidekiq/testing/inline.rb +2 -1
  44. data/lib/sidekiq/util.rb +17 -14
  45. data/lib/sidekiq/version.rb +2 -1
  46. data/lib/sidekiq/web.rb +41 -49
  47. data/lib/sidekiq/web/action.rb +14 -10
  48. data/lib/sidekiq/web/application.rb +60 -57
  49. data/lib/sidekiq/web/helpers.rb +66 -67
  50. data/lib/sidekiq/web/router.rb +17 -14
  51. data/lib/sidekiq/worker.rb +124 -97
  52. data/sidekiq.gemspec +16 -16
  53. data/web/assets/javascripts/dashboard.js +2 -21
  54. data/web/locales/ja.yml +2 -1
  55. metadata +19 -29
  56. data/.travis.yml +0 -11
  57. data/bin/sidekiqctl +0 -20
  58. data/lib/sidekiq/core_ext.rb +0 -1
  59. data/lib/sidekiq/ctl.rb +0 -221
  60. data/lib/sidekiq/logging.rb +0 -122
  61. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
@@ -1,19 +1,25 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/manager'
3
- require 'sidekiq/fetch'
4
- require 'sidekiq/scheduled'
2
+
3
+ require "sidekiq/manager"
4
+ require "sidekiq/fetch"
5
+ require "sidekiq/scheduled"
5
6
 
6
7
  module Sidekiq
7
- # The Launcher is a very simple Actor whose job is to
8
- # start, monitor and stop the core Actors in Sidekiq.
9
- # If any of these actors die, the Sidekiq process exits
10
- # immediately.
8
+ # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
11
9
  class Launcher
12
10
  include Util
13
11
 
14
- attr_accessor :manager, :poller, :fetcher
12
+ STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
15
13
 
16
- STATS_TTL = 5*365*24*60*60
14
+ PROCTITLES = [
15
+ proc { "sidekiq" },
16
+ proc { Sidekiq::VERSION },
17
+ proc { |me, data| data["tag"] },
18
+ proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data["concurrency"]} busy]" },
19
+ proc { |me, data| "stopping" if me.stopping? },
20
+ ]
21
+
22
+ attr_accessor :manager, :poller, :fetcher
17
23
 
18
24
  def initialize(options)
19
25
  @manager = Sidekiq::Manager.new(options)
@@ -62,10 +68,30 @@ module Sidekiq
62
68
 
63
69
  private unless $TESTING
64
70
 
71
+ def start_heartbeat
72
+ loop do
73
+ heartbeat
74
+ sleep 5
75
+ end
76
+ Sidekiq.logger.info("Heartbeat stopping...")
77
+ end
78
+
79
+ def clear_heartbeat
80
+ # Remove record from Redis since we are shutting down.
81
+ # Note we don't stop the heartbeat thread; if the process
82
+ # doesn't actually exit, it'll reappear in the Web UI.
83
+ Sidekiq.redis do |conn|
84
+ conn.pipelined do
85
+ conn.srem("processes", identity)
86
+ conn.del("#{identity}:workers")
87
+ end
88
+ end
89
+ rescue
90
+ # best effort, ignore network errors
91
+ end
92
+
65
93
  def heartbeat
66
- results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
67
- results.compact!
68
- $0 = results.join(' ')
94
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
69
95
 
70
96
 
71
97
  end
@@ -73,6 +99,7 @@ module Sidekiq
73
99
  def ❤
74
100
  key = identity
75
101
  fails = procd = 0
102
+
76
103
  begin
77
104
  fails = Processor::FAILURE.reset
78
105
  procd = Processor::PROCESSED.reset
@@ -80,6 +107,7 @@ module Sidekiq
80
107
 
81
108
  workers_key = "#{key}:workers"
82
109
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
110
+
83
111
  Sidekiq.redis do |conn|
84
112
  conn.multi do
85
113
  conn.incrby("stat:processed", procd)
@@ -97,24 +125,27 @@ module Sidekiq
97
125
  conn.expire(workers_key, 60)
98
126
  end
99
127
  end
128
+
100
129
  fails = procd = 0
101
130
 
102
- _, exists, _, _, msg = Sidekiq.redis do |conn|
103
- conn.multi do
104
- conn.sadd('processes', key)
131
+ _, exists, _, _, msg = Sidekiq.redis { |conn|
132
+ res = conn.multi {
133
+ conn.sadd("processes", key)
105
134
  conn.exists(key)
106
- conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
135
+ conn.hmset(key, "info", to_json, "busy", curstate.size, "beat", Time.now.to_f, "quiet", @done)
107
136
  conn.expire(key, 60)
108
137
  conn.rpop("#{key}-signals")
109
- end
110
- end
138
+ }
139
+
140
+ res
141
+ }
111
142
 
112
143
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
113
- fire_event(:heartbeat) if !exists
144
+ fire_event(:heartbeat) unless exists
114
145
 
115
146
  return unless msg
116
147
 
117
- ::Process.kill(msg, $$)
148
+ ::Process.kill(msg, ::Process.pid)
118
149
  rescue => e
119
150
  # ignore all redis/network issues
120
151
  logger.error("heartbeat: #{e.message}")
@@ -124,25 +155,17 @@ module Sidekiq
124
155
  end
125
156
  end
126
157
 
127
- def start_heartbeat
128
- while true
129
- heartbeat
130
- sleep 5
131
- end
132
- Sidekiq.logger.info("Heartbeat stopping...")
133
- end
134
-
135
158
  def to_data
136
159
  @data ||= begin
137
160
  {
138
- 'hostname' => hostname,
139
- 'started_at' => Time.now.to_f,
140
- 'pid' => $$,
141
- 'tag' => @options[:tag] || '',
142
- 'concurrency' => @options[:concurrency],
143
- 'queues' => @options[:queues].uniq,
144
- 'labels' => @options[:labels],
145
- 'identity' => identity,
161
+ "hostname" => hostname,
162
+ "started_at" => Time.now.to_f,
163
+ "pid" => ::Process.pid,
164
+ "tag" => @options[:tag] || "",
165
+ "concurrency" => @options[:concurrency],
166
+ "queues" => @options[:queues].uniq,
167
+ "labels" => @options[:labels],
168
+ "identity" => identity,
146
169
  }
147
170
  end
148
171
  end
@@ -154,20 +177,5 @@ module Sidekiq
154
177
  Sidekiq.dump_json(to_data)
155
178
  end
156
179
  end
157
-
158
- def clear_heartbeat
159
- # Remove record from Redis since we are shutting down.
160
- # Note we don't stop the heartbeat thread; if the process
161
- # doesn't actually exit, it'll reappear in the Web UI.
162
- Sidekiq.redis do |conn|
163
- conn.pipelined do
164
- conn.srem('processes', identity)
165
- conn.del("#{identity}:workers")
166
- end
167
- end
168
- rescue
169
- # best effort, ignore network errors
170
- end
171
-
172
180
  end
173
181
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "time"
5
+
6
+ module Sidekiq
7
+ class Logger < ::Logger
8
+ def initialize(*args)
9
+ super
10
+
11
+ self.formatter = Sidekiq.log_formatter
12
+ end
13
+
14
+ def with_context(hash)
15
+ ctx.merge!(hash)
16
+ yield
17
+ ensure
18
+ hash.keys.each { |key| ctx.delete(key) }
19
+ end
20
+
21
+ def ctx
22
+ Thread.current[:sidekiq_context] ||= {}
23
+ end
24
+
25
+ module Formatters
26
+ class Base < ::Logger::Formatter
27
+ def tid
28
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
29
+ end
30
+
31
+ def ctx
32
+ Thread.current[:sidekiq_context] ||= {}
33
+ end
34
+
35
+ def format_context
36
+ " " + ctx.compact.map { |k, v| "#{k}=#{v}" }.join(" ") if ctx.any?
37
+ end
38
+ end
39
+
40
+ class Pretty < Base
41
+ def call(severity, time, program_name, message)
42
+ "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
43
+ end
44
+ end
45
+
46
+ class WithoutTimestamp < Pretty
47
+ def call(severity, time, program_name, message)
48
+ "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
49
+ end
50
+ end
51
+
52
+ class JSON < Base
53
+ def call(severity, time, program_name, message)
54
+ hash = {
55
+ ts: time.utc.iso8601(3),
56
+ pid: ::Process.pid,
57
+ tid: tid,
58
+ lvl: severity,
59
+ msg: message,
60
+ }
61
+ c = ctx
62
+ hash["ctx"] = c unless c.empty?
63
+
64
+ Sidekiq.dump_json(hash) << "\n"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/util'
3
- require 'sidekiq/processor'
4
- require 'sidekiq/fetch'
5
- require 'thread'
6
- require 'set'
7
2
 
8
- module Sidekiq
3
+ require "sidekiq/util"
4
+ require "sidekiq/processor"
5
+ require "sidekiq/fetch"
6
+ require "set"
9
7
 
8
+ module Sidekiq
10
9
  ##
11
10
  # The Manager is the central coordination point in Sidekiq, controlling
12
11
  # the lifecycle of the Processors.
@@ -27,7 +26,7 @@ module Sidekiq
27
26
  attr_reader :workers
28
27
  attr_reader :options
29
28
 
30
- def initialize(options={})
29
+ def initialize(options = {})
31
30
  logger.debug { options.inspect }
32
31
  @options = options
33
32
  @count = options[:concurrency] || 10
@@ -113,7 +112,7 @@ module Sidekiq
113
112
  end
114
113
 
115
114
  if cleanup.size > 0
116
- jobs = cleanup.map {|p| p.job }.compact
115
+ jobs = cleanup.map { |p| p.job }.compact
117
116
 
118
117
  logger.warn { "Terminating #{cleanup.size} busy worker threads" }
119
118
  logger.warn { "Work still in progress #{jobs.inspect}" }
@@ -132,6 +131,5 @@ module Sidekiq
132
131
  processor.kill
133
132
  end
134
133
  end
135
-
136
134
  end
137
135
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Sidekiq
3
4
  # Middleware is code configured to run before/after
4
5
  # a message is processed. It is patterned after Rack
@@ -106,7 +107,7 @@ module Sidekiq
106
107
  i = entries.index { |entry| entry.klass == newklass }
107
108
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
108
109
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
109
- entries.insert(i+1, new_entry)
110
+ entries.insert(i + 1, new_entry)
110
111
  end
111
112
 
112
113
  def exists?(klass)
@@ -139,7 +140,7 @@ module Sidekiq
139
140
 
140
141
  def initialize(klass, *args)
141
142
  @klass = klass
142
- @args = args
143
+ @args = args
143
144
  end
144
145
 
145
146
  def make_new
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # Simple middleware to save the current locale and restore it when the job executes.
4
5
  # Use it by requiring it in your initializer:
@@ -9,19 +10,16 @@ module Sidekiq::Middleware::I18n
9
10
  # Get the current locale and store it in the message
10
11
  # to be sent to Sidekiq.
11
12
  class Client
12
- def call(worker_class, msg, queue, redis_pool)
13
- msg['locale'] ||= I18n.locale
13
+ def call(_worker, msg, _queue, _redis)
14
+ msg["locale"] ||= I18n.locale
14
15
  yield
15
16
  end
16
17
  end
17
18
 
18
19
  # Pull the msg locale out and set the current thread to use it.
19
20
  class Server
20
- def call(worker, msg, queue)
21
- I18n.locale = msg['locale'] || I18n.default_locale
22
- yield
23
- ensure
24
- I18n.locale = I18n.default_locale
21
+ def call(_worker, msg, _queue, &block)
22
+ I18n.with_locale(msg.fetch("locale", I18n.default_locale), &block)
25
23
  end
26
24
  end
27
25
  end
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "fileutils"
4
+ require "sidekiq/api"
5
+
6
+ class Sidekiq::Monitor
7
+ CMD = File.basename($PROGRAM_NAME)
8
+
9
+ attr_reader :stage
10
+
11
+ def self.print_usage
12
+ puts "#{CMD} - monitor Sidekiq from the command line."
13
+ puts
14
+ puts "Usage: #{CMD} status <section>"
15
+ puts
16
+ puts " <section> (optional) view a specific section of the status output"
17
+ puts " Valid sections are: #{Sidekiq::Monitor::Status::VALID_SECTIONS.join(", ")}"
18
+ puts
19
+ puts "Set REDIS_URL to the location of your Redis server if not monitoring localhost."
20
+ end
21
+
22
+ class Status
23
+ VALID_SECTIONS = %w[all version overview processes queues]
24
+ COL_PAD = 2
25
+
26
+ def display(section = nil)
27
+ section ||= "all"
28
+ unless VALID_SECTIONS.include? section
29
+ puts "I don't know how to check the status of '#{section}'!"
30
+ puts "Try one of these: #{VALID_SECTIONS.join(", ")}"
31
+ return
32
+ end
33
+ send(section)
34
+ rescue => e
35
+ puts "Couldn't get status: #{e}"
36
+ end
37
+
38
+ def all
39
+ version
40
+ puts
41
+ overview
42
+ puts
43
+ processes
44
+ puts
45
+ queues
46
+ end
47
+
48
+ def version
49
+ puts "Sidekiq #{Sidekiq::VERSION}"
50
+ puts Time.now
51
+ end
52
+
53
+ def overview
54
+ puts "---- Overview ----"
55
+ puts " Processed: #{delimit stats.processed}"
56
+ puts " Failed: #{delimit stats.failed}"
57
+ puts " Busy: #{delimit stats.workers_size}"
58
+ puts " Enqueued: #{delimit stats.enqueued}"
59
+ puts " Retries: #{delimit stats.retry_size}"
60
+ puts " Scheduled: #{delimit stats.scheduled_size}"
61
+ puts " Dead: #{delimit stats.dead_size}"
62
+ end
63
+
64
+ def processes
65
+ puts "---- Processes (#{process_set.size}) ----"
66
+ process_set.each_with_index do |process, index|
67
+ puts "#{process["identity"]} #{tags_for(process)}"
68
+ puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
69
+ puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
70
+ puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
71
+ puts "" unless (index + 1) == process_set.size
72
+ end
73
+ end
74
+
75
+ def queues
76
+ puts "---- Queues (#{queue_data.size}) ----"
77
+ columns = {
78
+ name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
79
+ size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
80
+ latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD],
81
+ }
82
+ columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
83
+ puts
84
+ queue_data.each do |q|
85
+ columns.each do |col, (dir, width)|
86
+ print q.send(col).public_send(dir, width)
87
+ end
88
+ puts
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def delimit(number)
95
+ number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
96
+ end
97
+
98
+ def split_multiline(values, opts = {})
99
+ return "none" unless values
100
+ pad = opts[:pad] || 0
101
+ max_length = opts[:max_length] || (80 - pad)
102
+ out = []
103
+ line = ""
104
+ values.each do |value|
105
+ if (line.length + value.length) > max_length
106
+ out << line
107
+ line = " " * pad
108
+ end
109
+ line << value + ", "
110
+ end
111
+ out << line[0..-3]
112
+ out.join("\n")
113
+ end
114
+
115
+ def tags_for(process)
116
+ tags = [
117
+ process["tag"],
118
+ process["labels"],
119
+ (process["quiet"] == "true" ? "quiet" : nil),
120
+ ].flatten.compact
121
+ tags.any? ? "[#{tags.join("] [")}]" : nil
122
+ end
123
+
124
+ def time_ago(timestamp)
125
+ seconds = Time.now - Time.at(timestamp)
126
+ return "just now" if seconds < 60
127
+ return "a minute ago" if seconds < 120
128
+ return "#{seconds.floor / 60} minutes ago" if seconds < 3600
129
+ return "an hour ago" if seconds < 7200
130
+ "#{seconds.floor / 60 / 60} hours ago"
131
+ end
132
+
133
+ QUEUE_STRUCT = Struct.new(:name, :size, :latency)
134
+ def queue_data
135
+ @queue_data ||= Sidekiq::Queue.all.map { |q|
136
+ QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf("%#.2f", q.latency))
137
+ }
138
+ end
139
+
140
+ def process_set
141
+ @process_set ||= Sidekiq::ProcessSet.new
142
+ end
143
+
144
+ def stats
145
+ @stats ||= Sidekiq::Stats.new
146
+ end
147
+ end
148
+ end