sidekiq 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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -0
  3. data/.github/contributing.md +32 -0
  4. data/.github/issue_template.md +11 -0
  5. data/.gitignore +13 -0
  6. data/.standard.yml +20 -0
  7. data/3.0-Upgrade.md +70 -0
  8. data/4.0-Upgrade.md +53 -0
  9. data/5.0-Upgrade.md +56 -0
  10. data/6.0-Upgrade.md +70 -0
  11. data/COMM-LICENSE +97 -0
  12. data/Changes.md +1570 -0
  13. data/Ent-2.0-Upgrade.md +37 -0
  14. data/Ent-Changes.md +250 -0
  15. data/Gemfile +24 -0
  16. data/Gemfile.lock +196 -0
  17. data/LICENSE +9 -0
  18. data/Pro-2.0-Upgrade.md +138 -0
  19. data/Pro-3.0-Upgrade.md +44 -0
  20. data/Pro-4.0-Upgrade.md +35 -0
  21. data/Pro-5.0-Upgrade.md +25 -0
  22. data/Pro-Changes.md +768 -0
  23. data/README.md +95 -0
  24. data/Rakefile +10 -0
  25. data/bin/sidekiq +18 -0
  26. data/bin/sidekiqload +153 -0
  27. data/bin/sidekiqmon +9 -0
  28. data/code_of_conduct.md +50 -0
  29. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  30. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  31. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  32. data/lib/generators/sidekiq/worker_generator.rb +47 -0
  33. data/lib/sidekiq.rb +248 -0
  34. data/lib/sidekiq/api.rb +927 -0
  35. data/lib/sidekiq/cli.rb +380 -0
  36. data/lib/sidekiq/client.rb +242 -0
  37. data/lib/sidekiq/delay.rb +41 -0
  38. data/lib/sidekiq/exception_handler.rb +27 -0
  39. data/lib/sidekiq/extensions/action_mailer.rb +47 -0
  40. data/lib/sidekiq/extensions/active_record.rb +42 -0
  41. data/lib/sidekiq/extensions/class_methods.rb +42 -0
  42. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  43. data/lib/sidekiq/fetch.rb +80 -0
  44. data/lib/sidekiq/job_logger.rb +55 -0
  45. data/lib/sidekiq/job_retry.rb +249 -0
  46. data/lib/sidekiq/launcher.rb +181 -0
  47. data/lib/sidekiq/logger.rb +69 -0
  48. data/lib/sidekiq/manager.rb +135 -0
  49. data/lib/sidekiq/middleware/chain.rb +151 -0
  50. data/lib/sidekiq/middleware/i18n.rb +40 -0
  51. data/lib/sidekiq/monitor.rb +148 -0
  52. data/lib/sidekiq/paginator.rb +42 -0
  53. data/lib/sidekiq/processor.rb +282 -0
  54. data/lib/sidekiq/rails.rb +52 -0
  55. data/lib/sidekiq/redis_connection.rb +138 -0
  56. data/lib/sidekiq/scheduled.rb +172 -0
  57. data/lib/sidekiq/testing.rb +332 -0
  58. data/lib/sidekiq/testing/inline.rb +30 -0
  59. data/lib/sidekiq/util.rb +69 -0
  60. data/lib/sidekiq/version.rb +5 -0
  61. data/lib/sidekiq/web.rb +205 -0
  62. data/lib/sidekiq/web/action.rb +93 -0
  63. data/lib/sidekiq/web/application.rb +356 -0
  64. data/lib/sidekiq/web/helpers.rb +324 -0
  65. data/lib/sidekiq/web/router.rb +103 -0
  66. data/lib/sidekiq/worker.rb +247 -0
  67. data/sidekiq.gemspec +21 -0
  68. data/web/assets/images/favicon.ico +0 -0
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +92 -0
  72. data/web/assets/javascripts/dashboard.js +296 -0
  73. data/web/assets/stylesheets/application-rtl.css +246 -0
  74. data/web/assets/stylesheets/application.css +1144 -0
  75. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  76. data/web/assets/stylesheets/bootstrap.css +5 -0
  77. data/web/locales/ar.yml +81 -0
  78. data/web/locales/cs.yml +78 -0
  79. data/web/locales/da.yml +68 -0
  80. data/web/locales/de.yml +69 -0
  81. data/web/locales/el.yml +68 -0
  82. data/web/locales/en.yml +81 -0
  83. data/web/locales/es.yml +70 -0
  84. data/web/locales/fa.yml +80 -0
  85. data/web/locales/fr.yml +78 -0
  86. data/web/locales/he.yml +79 -0
  87. data/web/locales/hi.yml +75 -0
  88. data/web/locales/it.yml +69 -0
  89. data/web/locales/ja.yml +81 -0
  90. data/web/locales/ko.yml +68 -0
  91. data/web/locales/nb.yml +77 -0
  92. data/web/locales/nl.yml +68 -0
  93. data/web/locales/pl.yml +59 -0
  94. data/web/locales/pt-br.yml +68 -0
  95. data/web/locales/pt.yml +67 -0
  96. data/web/locales/ru.yml +78 -0
  97. data/web/locales/sv.yml +68 -0
  98. data/web/locales/ta.yml +75 -0
  99. data/web/locales/uk.yml +76 -0
  100. data/web/locales/ur.yml +80 -0
  101. data/web/locales/zh-cn.yml +68 -0
  102. data/web/locales/zh-tw.yml +68 -0
  103. data/web/views/_footer.erb +20 -0
  104. data/web/views/_job_info.erb +88 -0
  105. data/web/views/_nav.erb +52 -0
  106. data/web/views/_paging.erb +23 -0
  107. data/web/views/_poll_link.erb +7 -0
  108. data/web/views/_status.erb +4 -0
  109. data/web/views/_summary.erb +40 -0
  110. data/web/views/busy.erb +98 -0
  111. data/web/views/dashboard.erb +75 -0
  112. data/web/views/dead.erb +34 -0
  113. data/web/views/layout.erb +40 -0
  114. data/web/views/morgue.erb +75 -0
  115. data/web/views/queue.erb +46 -0
  116. data/web/views/queues.erb +30 -0
  117. data/web/views/retries.erb +80 -0
  118. data/web/views/retry.erb +34 -0
  119. data/web/views/scheduled.erb +54 -0
  120. data/web/views/scheduled_job_info.erb +8 -0
  121. metadata +220 -0
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/manager"
4
+ require "sidekiq/fetch"
5
+ require "sidekiq/scheduled"
6
+
7
+ module Sidekiq
8
+ # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
9
+ class Launcher
10
+ include Util
11
+
12
+ STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
13
+
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
23
+
24
+ def initialize(options)
25
+ @manager = Sidekiq::Manager.new(options)
26
+ @poller = Sidekiq::Scheduled::Poller.new
27
+ @done = false
28
+ @options = options
29
+ end
30
+
31
+ def run
32
+ @thread = safe_thread("heartbeat", &method(:start_heartbeat))
33
+ @poller.start
34
+ @manager.start
35
+ end
36
+
37
+ # Stops this instance from processing any more jobs,
38
+ #
39
+ def quiet
40
+ @done = true
41
+ @manager.quiet
42
+ @poller.terminate
43
+ end
44
+
45
+ # Shuts down the process. This method does not
46
+ # return until all work is complete and cleaned up.
47
+ # It can take up to the timeout to complete.
48
+ def stop
49
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
50
+
51
+ @done = true
52
+ @manager.quiet
53
+ @poller.terminate
54
+
55
+ @manager.stop(deadline)
56
+
57
+ # Requeue everything in case there was a worker who grabbed work while stopped
58
+ # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
59
+ strategy = (@options[:fetch] || Sidekiq::BasicFetch)
60
+ strategy.bulk_requeue([], @options)
61
+
62
+ clear_heartbeat
63
+ end
64
+
65
+ def stopping?
66
+ @done
67
+ end
68
+
69
+ private unless $TESTING
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
+
93
+ def heartbeat
94
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
95
+
96
+
97
+ end
98
+
99
+ def ❤
100
+ key = identity
101
+ fails = procd = 0
102
+
103
+ begin
104
+ fails = Processor::FAILURE.reset
105
+ procd = Processor::PROCESSED.reset
106
+ curstate = Processor::WORKER_STATE.dup
107
+
108
+ workers_key = "#{key}:workers"
109
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
110
+
111
+ Sidekiq.redis do |conn|
112
+ conn.multi do
113
+ conn.incrby("stat:processed", procd)
114
+ conn.incrby("stat:processed:#{nowdate}", procd)
115
+ conn.expire("stat:processed:#{nowdate}", STATS_TTL)
116
+
117
+ conn.incrby("stat:failed", fails)
118
+ conn.incrby("stat:failed:#{nowdate}", fails)
119
+ conn.expire("stat:failed:#{nowdate}", STATS_TTL)
120
+
121
+ conn.del(workers_key)
122
+ curstate.each_pair do |tid, hash|
123
+ conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
124
+ end
125
+ conn.expire(workers_key, 60)
126
+ end
127
+ end
128
+
129
+ fails = procd = 0
130
+
131
+ _, exists, _, _, msg = Sidekiq.redis { |conn|
132
+ res = conn.multi {
133
+ conn.sadd("processes", key)
134
+ conn.exists(key)
135
+ conn.hmset(key, "info", to_json, "busy", curstate.size, "beat", Time.now.to_f, "quiet", @done)
136
+ conn.expire(key, 60)
137
+ conn.rpop("#{key}-signals")
138
+ }
139
+
140
+ res
141
+ }
142
+
143
+ # first heartbeat or recovering from an outage and need to reestablish our heartbeat
144
+ fire_event(:heartbeat) unless exists
145
+
146
+ return unless msg
147
+
148
+ ::Process.kill(msg, ::Process.pid)
149
+ rescue => e
150
+ # ignore all redis/network issues
151
+ logger.error("heartbeat: #{e.message}")
152
+ # don't lose the counts if there was a network issue
153
+ Processor::PROCESSED.incr(procd)
154
+ Processor::FAILURE.incr(fails)
155
+ end
156
+ end
157
+
158
+ def to_data
159
+ @data ||= begin
160
+ {
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,
169
+ }
170
+ end
171
+ end
172
+
173
+ def to_json
174
+ @json ||= begin
175
+ # this data changes infrequently so dump it to a string
176
+ # now so we don't need to dump it every heartbeat.
177
+ Sidekiq.dump_json(to_data)
178
+ end
179
+ end
180
+ end
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
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/util"
4
+ require "sidekiq/processor"
5
+ require "sidekiq/fetch"
6
+ require "set"
7
+
8
+ module Sidekiq
9
+ ##
10
+ # The Manager is the central coordination point in Sidekiq, controlling
11
+ # the lifecycle of the Processors.
12
+ #
13
+ # Tasks:
14
+ #
15
+ # 1. start: Spin up Processors.
16
+ # 3. processor_died: Handle job failure, throw away Processor, create new one.
17
+ # 4. quiet: shutdown idle Processors.
18
+ # 5. stop: hard stop the Processors by deadline.
19
+ #
20
+ # Note that only the last task requires its own Thread since it has to monitor
21
+ # the shutdown process. The other tasks are performed by other threads.
22
+ #
23
+ class Manager
24
+ include Util
25
+
26
+ attr_reader :workers
27
+ attr_reader :options
28
+
29
+ def initialize(options = {})
30
+ logger.debug { options.inspect }
31
+ @options = options
32
+ @count = options[:concurrency] || 10
33
+ raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
34
+
35
+ @done = false
36
+ @workers = Set.new
37
+ @count.times do
38
+ @workers << Processor.new(self)
39
+ end
40
+ @plock = Mutex.new
41
+ end
42
+
43
+ def start
44
+ @workers.each do |x|
45
+ x.start
46
+ end
47
+ end
48
+
49
+ def quiet
50
+ return if @done
51
+ @done = true
52
+
53
+ logger.info { "Terminating quiet workers" }
54
+ @workers.each { |x| x.terminate }
55
+ fire_event(:quiet, reverse: true)
56
+ end
57
+
58
+ # hack for quicker development / testing environment #2774
59
+ PAUSE_TIME = STDOUT.tty? ? 0.1 : 0.5
60
+
61
+ def stop(deadline)
62
+ quiet
63
+ fire_event(:shutdown, reverse: true)
64
+
65
+ # some of the shutdown events can be async,
66
+ # we don't have any way to know when they're done but
67
+ # give them a little time to take effect
68
+ sleep PAUSE_TIME
69
+ return if @workers.empty?
70
+
71
+ logger.info { "Pausing to allow workers to finish..." }
72
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
73
+ while remaining > PAUSE_TIME
74
+ return if @workers.empty?
75
+ sleep PAUSE_TIME
76
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
77
+ end
78
+ return if @workers.empty?
79
+
80
+ hard_shutdown
81
+ end
82
+
83
+ def processor_stopped(processor)
84
+ @plock.synchronize do
85
+ @workers.delete(processor)
86
+ end
87
+ end
88
+
89
+ def processor_died(processor, reason)
90
+ @plock.synchronize do
91
+ @workers.delete(processor)
92
+ unless @done
93
+ p = Processor.new(self)
94
+ @workers << p
95
+ p.start
96
+ end
97
+ end
98
+ end
99
+
100
+ def stopped?
101
+ @done
102
+ end
103
+
104
+ private
105
+
106
+ def hard_shutdown
107
+ # We've reached the timeout and we still have busy workers.
108
+ # They must die but their jobs shall live on.
109
+ cleanup = nil
110
+ @plock.synchronize do
111
+ cleanup = @workers.dup
112
+ end
113
+
114
+ if cleanup.size > 0
115
+ jobs = cleanup.map { |p| p.job }.compact
116
+
117
+ logger.warn { "Terminating #{cleanup.size} busy worker threads" }
118
+ logger.warn { "Work still in progress #{jobs.inspect}" }
119
+
120
+ # Re-enqueue unfinished jobs
121
+ # NOTE: You may notice that we may push a job back to redis before
122
+ # the worker thread is terminated. This is ok because Sidekiq's
123
+ # contract says that jobs are run AT LEAST once. Process termination
124
+ # is delayed until we're certain the jobs are back in Redis because
125
+ # it is worse to lose a job than to run it twice.
126
+ strategy = (@options[:fetch] || Sidekiq::BasicFetch)
127
+ strategy.bulk_requeue(jobs, @options)
128
+ end
129
+
130
+ cleanup.each do |processor|
131
+ processor.kill
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ # Middleware is code configured to run before/after
5
+ # a message is processed. It is patterned after Rack
6
+ # middleware. Middleware exists for the client side
7
+ # (pushing jobs onto the queue) as well as the server
8
+ # side (when jobs are actually processed).
9
+ #
10
+ # To add middleware for the client:
11
+ #
12
+ # Sidekiq.configure_client do |config|
13
+ # config.client_middleware do |chain|
14
+ # chain.add MyClientHook
15
+ # end
16
+ # end
17
+ #
18
+ # To modify middleware for the server, just call
19
+ # with another block:
20
+ #
21
+ # Sidekiq.configure_server do |config|
22
+ # config.server_middleware do |chain|
23
+ # chain.add MyServerHook
24
+ # chain.remove ActiveRecord
25
+ # end
26
+ # end
27
+ #
28
+ # To insert immediately preceding another entry:
29
+ #
30
+ # Sidekiq.configure_client do |config|
31
+ # config.client_middleware do |chain|
32
+ # chain.insert_before ActiveRecord, MyClientHook
33
+ # end
34
+ # end
35
+ #
36
+ # To insert immediately after another entry:
37
+ #
38
+ # Sidekiq.configure_client do |config|
39
+ # config.client_middleware do |chain|
40
+ # chain.insert_after ActiveRecord, MyClientHook
41
+ # end
42
+ # end
43
+ #
44
+ # This is an example of a minimal server middleware:
45
+ #
46
+ # class MyServerHook
47
+ # def call(worker_instance, msg, queue)
48
+ # puts "Before work"
49
+ # yield
50
+ # puts "After work"
51
+ # end
52
+ # end
53
+ #
54
+ # This is an example of a minimal client middleware, note
55
+ # the method must return the result or the job will not push
56
+ # to Redis:
57
+ #
58
+ # class MyClientHook
59
+ # def call(worker_class, msg, queue, redis_pool)
60
+ # puts "Before push"
61
+ # result = yield
62
+ # puts "After push"
63
+ # result
64
+ # end
65
+ # end
66
+ #
67
+ module Middleware
68
+ class Chain
69
+ include Enumerable
70
+ attr_reader :entries
71
+
72
+ def initialize_copy(copy)
73
+ copy.instance_variable_set(:@entries, entries.dup)
74
+ end
75
+
76
+ def each(&block)
77
+ entries.each(&block)
78
+ end
79
+
80
+ def initialize
81
+ @entries = []
82
+ yield self if block_given?
83
+ end
84
+
85
+ def remove(klass)
86
+ entries.delete_if { |entry| entry.klass == klass }
87
+ end
88
+
89
+ def add(klass, *args)
90
+ remove(klass) if exists?(klass)
91
+ entries << Entry.new(klass, *args)
92
+ end
93
+
94
+ def prepend(klass, *args)
95
+ remove(klass) if exists?(klass)
96
+ entries.insert(0, Entry.new(klass, *args))
97
+ end
98
+
99
+ def insert_before(oldklass, newklass, *args)
100
+ i = entries.index { |entry| entry.klass == newklass }
101
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
102
+ i = entries.index { |entry| entry.klass == oldklass } || 0
103
+ entries.insert(i, new_entry)
104
+ end
105
+
106
+ def insert_after(oldklass, newklass, *args)
107
+ i = entries.index { |entry| entry.klass == newklass }
108
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
109
+ i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
110
+ entries.insert(i + 1, new_entry)
111
+ end
112
+
113
+ def exists?(klass)
114
+ any? { |entry| entry.klass == klass }
115
+ end
116
+
117
+ def retrieve
118
+ map(&:make_new)
119
+ end
120
+
121
+ def clear
122
+ entries.clear
123
+ end
124
+
125
+ def invoke(*args)
126
+ chain = retrieve.dup
127
+ traverse_chain = lambda do
128
+ if chain.empty?
129
+ yield
130
+ else
131
+ chain.shift.call(*args, &traverse_chain)
132
+ end
133
+ end
134
+ traverse_chain.call
135
+ end
136
+ end
137
+
138
+ class Entry
139
+ attr_reader :klass
140
+
141
+ def initialize(klass, *args)
142
+ @klass = klass
143
+ @args = args
144
+ end
145
+
146
+ def make_new
147
+ @klass.new(*@args)
148
+ end
149
+ end
150
+ end
151
+ end