sidekiq 3.4.1 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (235) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +1118 -4
  3. data/LICENSE.txt +9 -0
  4. data/README.md +55 -47
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +26 -3
  7. data/bin/sidekiqload +247 -0
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/generators/sidekiq/job_generator.rb +57 -0
  10. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  11. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  12. data/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  13. data/lib/sidekiq/api.rb +714 -312
  14. data/lib/sidekiq/capsule.rb +130 -0
  15. data/lib/sidekiq/cli.rb +275 -241
  16. data/lib/sidekiq/client.rb +141 -110
  17. data/lib/sidekiq/component.rb +68 -0
  18. data/lib/sidekiq/config.rb +291 -0
  19. data/lib/sidekiq/deploy.rb +62 -0
  20. data/lib/sidekiq/embedded.rb +61 -0
  21. data/lib/sidekiq/fetch.rb +53 -121
  22. data/lib/sidekiq/iterable_job.rb +53 -0
  23. data/lib/sidekiq/job/interrupt_handler.rb +22 -0
  24. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  25. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  26. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  27. data/lib/sidekiq/job/iterable.rb +231 -0
  28. data/lib/sidekiq/job.rb +385 -0
  29. data/lib/sidekiq/job_logger.rb +64 -0
  30. data/lib/sidekiq/job_retry.rb +305 -0
  31. data/lib/sidekiq/job_util.rb +107 -0
  32. data/lib/sidekiq/launcher.rb +241 -66
  33. data/lib/sidekiq/logger.rb +131 -0
  34. data/lib/sidekiq/manager.rb +91 -192
  35. data/lib/sidekiq/metrics/query.rb +156 -0
  36. data/lib/sidekiq/metrics/shared.rb +95 -0
  37. data/lib/sidekiq/metrics/tracking.rb +140 -0
  38. data/lib/sidekiq/middleware/chain.rb +114 -56
  39. data/lib/sidekiq/middleware/current_attributes.rb +111 -0
  40. data/lib/sidekiq/middleware/i18n.rb +8 -7
  41. data/lib/sidekiq/middleware/modules.rb +21 -0
  42. data/lib/sidekiq/monitor.rb +146 -0
  43. data/lib/sidekiq/paginator.rb +29 -16
  44. data/lib/sidekiq/processor.rb +248 -112
  45. data/lib/sidekiq/rails.rb +61 -27
  46. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  47. data/lib/sidekiq/redis_connection.rb +68 -48
  48. data/lib/sidekiq/ring_buffer.rb +29 -0
  49. data/lib/sidekiq/scheduled.rb +173 -52
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +24 -0
  52. data/lib/sidekiq/testing/inline.rb +7 -5
  53. data/lib/sidekiq/testing.rb +206 -65
  54. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  55. data/lib/sidekiq/version.rb +4 -1
  56. data/lib/sidekiq/web/action.rb +99 -0
  57. data/lib/sidekiq/web/application.rb +479 -0
  58. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  59. data/lib/sidekiq/web/helpers.rb +415 -0
  60. data/lib/sidekiq/web/router.rb +104 -0
  61. data/lib/sidekiq/web.rb +158 -200
  62. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  63. data/lib/sidekiq.rb +100 -132
  64. data/sidekiq.gemspec +27 -23
  65. data/web/assets/images/apple-touch-icon.png +0 -0
  66. data/web/assets/images/favicon.ico +0 -0
  67. data/web/assets/javascripts/application.js +177 -72
  68. data/web/assets/javascripts/base-charts.js +106 -0
  69. data/web/assets/javascripts/chart.min.js +13 -0
  70. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  71. data/web/assets/javascripts/dashboard-charts.js +192 -0
  72. data/web/assets/javascripts/dashboard.js +37 -286
  73. data/web/assets/javascripts/metrics.js +298 -0
  74. data/web/assets/stylesheets/application-dark.css +147 -0
  75. data/web/assets/stylesheets/application-rtl.css +163 -0
  76. data/web/assets/stylesheets/application.css +228 -247
  77. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  78. data/web/assets/stylesheets/bootstrap.css +4 -8
  79. data/web/locales/ar.yml +87 -0
  80. data/web/locales/cs.yml +62 -52
  81. data/web/locales/da.yml +60 -53
  82. data/web/locales/de.yml +65 -53
  83. data/web/locales/el.yml +43 -24
  84. data/web/locales/en.yml +86 -61
  85. data/web/locales/es.yml +70 -53
  86. data/web/locales/fa.yml +80 -0
  87. data/web/locales/fr.yml +86 -56
  88. data/web/locales/gd.yml +99 -0
  89. data/web/locales/he.yml +80 -0
  90. data/web/locales/hi.yml +59 -59
  91. data/web/locales/it.yml +53 -53
  92. data/web/locales/ja.yml +78 -56
  93. data/web/locales/ko.yml +52 -52
  94. data/web/locales/lt.yml +83 -0
  95. data/web/locales/{no.yml → nb.yml} +62 -54
  96. data/web/locales/nl.yml +52 -52
  97. data/web/locales/pl.yml +45 -45
  98. data/web/locales/pt-br.yml +83 -55
  99. data/web/locales/pt.yml +51 -51
  100. data/web/locales/ru.yml +68 -60
  101. data/web/locales/sv.yml +53 -53
  102. data/web/locales/ta.yml +60 -60
  103. data/web/locales/tr.yml +101 -0
  104. data/web/locales/uk.yml +77 -0
  105. data/web/locales/ur.yml +80 -0
  106. data/web/locales/vi.yml +83 -0
  107. data/web/locales/zh-cn.yml +43 -16
  108. data/web/locales/zh-tw.yml +42 -8
  109. data/web/views/_footer.erb +22 -9
  110. data/web/views/_job_info.erb +27 -6
  111. data/web/views/_metrics_period_select.erb +12 -0
  112. data/web/views/_nav.erb +8 -22
  113. data/web/views/_paging.erb +3 -1
  114. data/web/views/_poll_link.erb +4 -0
  115. data/web/views/_summary.erb +7 -7
  116. data/web/views/busy.erb +91 -31
  117. data/web/views/dashboard.erb +52 -22
  118. data/web/views/dead.erb +5 -4
  119. data/web/views/filtering.erb +7 -0
  120. data/web/views/layout.erb +19 -7
  121. data/web/views/metrics.erb +91 -0
  122. data/web/views/metrics_for_job.erb +59 -0
  123. data/web/views/morgue.erb +26 -20
  124. data/web/views/queue.erb +36 -25
  125. data/web/views/queues.erb +24 -7
  126. data/web/views/retries.erb +29 -21
  127. data/web/views/retry.erb +6 -5
  128. data/web/views/scheduled.erb +20 -17
  129. data/web/views/scheduled_job_info.erb +2 -1
  130. metadata +101 -232
  131. data/.gitignore +0 -12
  132. data/.travis.yml +0 -16
  133. data/3.0-Upgrade.md +0 -70
  134. data/COMM-LICENSE +0 -85
  135. data/Contributing.md +0 -32
  136. data/Gemfile +0 -22
  137. data/LICENSE +0 -9
  138. data/Pro-2.0-Upgrade.md +0 -138
  139. data/Pro-Changes.md +0 -412
  140. data/Rakefile +0 -9
  141. data/bin/sidekiqctl +0 -93
  142. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  143. data/lib/generators/sidekiq/templates/worker_test.rb.erb +0 -8
  144. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  145. data/lib/sidekiq/actor.rb +0 -39
  146. data/lib/sidekiq/core_ext.rb +0 -105
  147. data/lib/sidekiq/exception_handler.rb +0 -30
  148. data/lib/sidekiq/extensions/action_mailer.rb +0 -56
  149. data/lib/sidekiq/extensions/active_record.rb +0 -39
  150. data/lib/sidekiq/extensions/class_methods.rb +0 -39
  151. data/lib/sidekiq/extensions/generic_proxy.rb +0 -24
  152. data/lib/sidekiq/logging.rb +0 -104
  153. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  154. data/lib/sidekiq/middleware/server/logging.rb +0 -35
  155. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
  156. data/lib/sidekiq/util.rb +0 -55
  157. data/lib/sidekiq/web_helpers.rb +0 -234
  158. data/lib/sidekiq/worker.rb +0 -89
  159. data/test/config.yml +0 -9
  160. data/test/env_based_config.yml +0 -11
  161. data/test/fake_env.rb +0 -0
  162. data/test/fixtures/en.yml +0 -2
  163. data/test/helper.rb +0 -39
  164. data/test/test_api.rb +0 -494
  165. data/test/test_cli.rb +0 -365
  166. data/test/test_client.rb +0 -269
  167. data/test/test_exception_handler.rb +0 -55
  168. data/test/test_extensions.rb +0 -120
  169. data/test/test_fetch.rb +0 -104
  170. data/test/test_logging.rb +0 -34
  171. data/test/test_manager.rb +0 -164
  172. data/test/test_middleware.rb +0 -159
  173. data/test/test_processor.rb +0 -166
  174. data/test/test_redis_connection.rb +0 -127
  175. data/test/test_retry.rb +0 -373
  176. data/test/test_scheduled.rb +0 -120
  177. data/test/test_scheduling.rb +0 -71
  178. data/test/test_sidekiq.rb +0 -69
  179. data/test/test_testing.rb +0 -82
  180. data/test/test_testing_fake.rb +0 -271
  181. data/test/test_testing_inline.rb +0 -93
  182. data/test/test_web.rb +0 -594
  183. data/test/test_web_helpers.rb +0 -52
  184. data/test/test_worker_generator.rb +0 -17
  185. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  186. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  187. data/web/assets/images/status/active.png +0 -0
  188. data/web/assets/images/status/idle.png +0 -0
  189. data/web/assets/javascripts/locales/README.md +0 -27
  190. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  191. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  192. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  193. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  194. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  195. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  196. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  197. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  198. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  199. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  200. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  201. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  202. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  203. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  204. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  205. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  206. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  207. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  208. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  209. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  210. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  211. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  212. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  213. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  214. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  215. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  216. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  217. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  218. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  219. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  220. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  221. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  222. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  223. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  224. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  225. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  226. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  227. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  228. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  229. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  230. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  231. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  232. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  233. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  234. data/web/views/_poll.erb +0 -10
  235. /data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
data/lib/sidekiq/cli.rb CHANGED
@@ -1,256 +1,320 @@
1
- # encoding: utf-8
2
- $stdout.sync = true
1
+ # frozen_string_literal: true
3
2
 
4
- require 'yaml'
5
- require 'singleton'
6
- require 'optparse'
7
- require 'erb'
8
- require 'fileutils'
3
+ $stdout.sync = true
9
4
 
10
- require 'sidekiq'
11
- require 'sidekiq/util'
5
+ require "yaml"
6
+ require "singleton"
7
+ require "optparse"
8
+ require "erb"
9
+ require "fileutils"
12
10
 
13
- module Sidekiq
14
- # We are shutting down Sidekiq but what about workers that
15
- # are working on some long job? This error is
16
- # raised in workers that have not finished within the hard
17
- # timeout limit. This is needed to rollback db transactions,
18
- # otherwise Ruby's Thread#kill will commit. See #377.
19
- # DO NOT RESCUE THIS ERROR.
20
- class Shutdown < Interrupt; end
11
+ require "sidekiq"
12
+ require "sidekiq/config"
13
+ require "sidekiq/component"
14
+ require "sidekiq/capsule"
15
+ require "sidekiq/launcher"
21
16
 
17
+ module Sidekiq # :nodoc:
22
18
  class CLI
23
- include Util
19
+ include Sidekiq::Component
24
20
  include Singleton unless $TESTING
25
21
 
26
- # Used for CLI testing
27
- attr_accessor :code
28
22
  attr_accessor :launcher
29
23
  attr_accessor :environment
24
+ attr_accessor :config
30
25
 
31
- def initialize
32
- @code = nil
33
- end
34
-
35
- def parse(args=ARGV)
36
- @code = nil
26
+ def parse(args = ARGV.dup)
27
+ @config ||= Sidekiq.default_configuration
37
28
 
38
29
  setup_options(args)
39
30
  initialize_logger
40
31
  validate!
41
- daemonize
42
- write_pid
43
- load_celluloid
32
+ end
33
+
34
+ def jruby?
35
+ defined?(::JRUBY_VERSION)
44
36
  end
45
37
 
46
38
  # Code within this method is not tested because it alters
47
39
  # global process state irreversibly. PRs which improve the
48
40
  # test coverage of Sidekiq::CLI are welcomed.
49
- def run
50
- boot_system
51
- print_banner
41
+ def run(boot_app: true, warmup: true)
42
+ boot_application if boot_app
52
43
 
53
- self_read, self_write = IO.pipe
44
+ if environment == "development" && $stdout.tty? && @config.logger.formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
45
+ print_banner
46
+ end
47
+ logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
54
48
 
55
- %w(INT TERM USR1 USR2 TTIN).each do |sig|
56
- begin
57
- trap sig do
58
- self_write.puts(sig)
49
+ self_read, self_write = IO.pipe
50
+ sigs = %w[INT TERM TTIN TSTP]
51
+ # USR1 and USR2 don't work on the JVM
52
+ sigs << "USR2" if Sidekiq.pro? && !jruby?
53
+ sigs.each do |sig|
54
+ old_handler = Signal.trap(sig) do
55
+ if old_handler.respond_to?(:call)
56
+ begin
57
+ old_handler.call
58
+ rescue Exception => exc
59
+ # signal handlers can't use Logger so puts only
60
+ puts ["Error in #{sig} handler", exc].inspect
61
+ end
59
62
  end
60
- rescue ArgumentError
61
- puts "Signal #{sig} not supported"
63
+ self_write.puts(sig)
62
64
  end
65
+ rescue ArgumentError
66
+ puts "Signal #{sig} not supported"
63
67
  end
64
68
 
65
69
  logger.info "Running in #{RUBY_DESCRIPTION}"
66
70
  logger.info Sidekiq::LICENSE
67
- logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org/pro" unless defined?(::Sidekiq::Pro)
71
+ logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
72
+
73
+ # touch the connection pool so it is created before we
74
+ # fire startup and start multithreading.
75
+ info = @config.redis_info
76
+ ver = Gem::Version.new(info["redis_version"])
77
+ raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
78
+
79
+ maxmemory_policy = info["maxmemory_policy"]
80
+ if maxmemory_policy != "noeviction" && maxmemory_policy != ""
81
+ # Redis Enterprise Cloud returns "" for their policy 😳
82
+ logger.warn <<~EOM
83
+
84
+
85
+ WARNING: Your Redis instance will evict Sidekiq data under heavy load.
86
+ The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
87
+ See: https://github.com/sidekiq/sidekiq/wiki/Using-Redis#memory
68
88
 
69
- fire_event(:startup)
89
+ EOM
90
+ end
70
91
 
71
- Sidekiq.redis do |conn|
72
- # touch the connection pool so it is created before we
73
- # launch the actors.
92
+ # Since the user can pass us a connection pool explicitly in the initializer, we
93
+ # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
94
+ @config.capsules.each_pair do |name, cap|
95
+ raise ArgumentError, "Pool size too small for #{name}" if cap.redis_pool.size < cap.concurrency
74
96
  end
75
97
 
76
- if !options[:daemon]
77
- logger.info 'Starting processing, hit Ctrl-C to stop'
98
+ # cache process identity
99
+ @config[:identity] = identity
100
+
101
+ # Touch middleware so it isn't lazy loaded by multiple threads, #3043
102
+ @config.server_middleware
103
+
104
+ ::Process.warmup if warmup && ::Process.respond_to?(:warmup)
105
+
106
+ # Before this point, the process is initializing with just the main thread.
107
+ # Starting here the process will now have multiple threads running.
108
+ fire_event(:startup, reverse: false, reraise: true)
109
+
110
+ logger.debug { "Client Middleware: #{@config.default_capsule.client_middleware.map(&:klass).join(", ")}" }
111
+ logger.debug { "Server Middleware: #{@config.default_capsule.server_middleware.map(&:klass).join(", ")}" }
112
+
113
+ launch(self_read)
114
+ end
115
+
116
+ def launch(self_read)
117
+ if environment == "development" && $stdout.tty?
118
+ logger.info "Starting processing, hit Ctrl-C to stop"
78
119
  end
79
120
 
80
- require 'sidekiq/launcher'
81
- @launcher = Sidekiq::Launcher.new(options)
121
+ @launcher = Sidekiq::Launcher.new(@config)
82
122
 
83
123
  begin
84
124
  launcher.run
85
125
 
86
- while readable_io = IO.select([self_read])
87
- signal = readable_io.first[0].gets.strip
126
+ while self_read.wait_readable
127
+ signal = self_read.gets.strip
88
128
  handle_signal(signal)
89
129
  end
90
130
  rescue Interrupt
91
- logger.info 'Shutting down'
131
+ logger.info "Shutting down"
92
132
  launcher.stop
93
- fire_event(:shutdown, true)
94
- # Explicitly exit so busy Processor threads can't block
95
- # process shutdown.
133
+ logger.info "Bye!"
134
+
135
+ # Explicitly exit so busy Processor threads won't block process shutdown.
136
+ #
137
+ # NB: slow at_exit handlers will prevent a timely exit if they take
138
+ # a while to run. If Sidekiq is getting here but the process isn't exiting,
139
+ # use the TTIN signal to determine where things are stuck.
96
140
  exit(0)
97
141
  end
98
142
  end
99
143
 
144
+ HOLIDAY_COLORS = {
145
+ # got other color-specific holidays from around the world?
146
+ # https://developer-book.com/post/definitive-guide-for-colored-text-in-terminal/#256-color-escape-codes
147
+ "3-17" => "\e[1;32m", # St. Patrick's Day green
148
+ "10-31" => "\e[38;5;208m" # Halloween orange
149
+ }
150
+
151
+ def self.day
152
+ @@day ||= begin
153
+ t = Date.today
154
+ "#{t.month}-#{t.day}"
155
+ end
156
+ end
157
+
158
+ def self.r
159
+ @@r ||= HOLIDAY_COLORS[day] || "\e[1;31m"
160
+ end
161
+
162
+ def self.b
163
+ @@b ||= HOLIDAY_COLORS[day] || "\e[30m"
164
+ end
165
+
166
+ def self.w
167
+ "\e[1;37m"
168
+ end
169
+
170
+ def self.reset
171
+ @@b = @@r = @@day = nil
172
+ "\e[0m"
173
+ end
174
+
100
175
  def self.banner
101
- %q{ s
102
- ss
103
- sss sss ss
104
- s sss s ssss sss ____ _ _ _ _
105
- s sssss ssss / ___|(_) __| | ___| | _(_) __ _
106
- s sss \___ \| |/ _` |/ _ \ |/ / |/ _` |
107
- s sssss s ___) | | (_| | __/ <| | (_| |
108
- ss s s |____/|_|\__,_|\___|_|\_\_|\__, |
109
- s s s |_|
110
- s s
111
- sss
112
- sss }
176
+ %{
177
+ #{w} m,
178
+ #{w} `$b
179
+ #{w} .ss, $$: .,d$
180
+ #{w} `$$P,d$P' .,md$P"'
181
+ #{w} ,$$$$$b#{b}/#{w}md$$$P^'
182
+ #{w} .d$$$$$$#{b}/#{w}$$$P'
183
+ #{w} $$^' `"#{b}/#{w}$$$' #{r}____ _ _ _ _
184
+ #{w} $: #{b}'#{w},$$: #{r} / ___|(_) __| | ___| | _(_) __ _
185
+ #{w} `b :$$ #{r} \\___ \\| |/ _` |/ _ \\ |/ / |/ _` |
186
+ #{w} $$: #{r} ___) | | (_| | __/ <| | (_| |
187
+ #{w} $$ #{r}|____/|_|\\__,_|\\___|_|\\_\\_|\\__, |
188
+ #{w} .d$$ #{r} |_|
189
+ #{reset}}
113
190
  end
114
191
 
115
- def handle_signal(sig)
116
- Sidekiq.logger.debug "Got #{sig} signal"
117
- case sig
118
- when 'INT'
119
- # Handle Ctrl-C in JRuby like MRI
120
- # http://jira.codehaus.org/browse/JRUBY-4637
121
- raise Interrupt
122
- when 'TERM'
123
- # Heroku sends TERM and then waits 10 seconds for process to exit.
124
- raise Interrupt
125
- when 'USR1'
126
- Sidekiq.logger.info "Received USR1, no longer accepting new work"
127
- launcher.manager.async.stop
128
- fire_event(:quiet, true)
129
- when 'USR2'
130
- if Sidekiq.options[:logfile]
131
- Sidekiq.logger.info "Received USR2, reopening log file"
132
- Sidekiq::Logging.reopen_logs
133
- end
134
- when 'TTIN'
192
+ SIGNAL_HANDLERS = {
193
+ # Ctrl-C in terminal
194
+ "INT" => ->(cli) { raise Interrupt },
195
+ # TERM is the signal that Sidekiq must exit.
196
+ # Heroku sends TERM and then waits 30 seconds for process to exit.
197
+ "TERM" => ->(cli) { raise Interrupt },
198
+ "TSTP" => ->(cli) {
199
+ cli.logger.info "Received TSTP, no longer accepting new work"
200
+ cli.launcher.quiet
201
+ },
202
+ "TTIN" => ->(cli) {
135
203
  Thread.list.each do |thread|
136
- Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
204
+ cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
137
205
  if thread.backtrace
138
- Sidekiq.logger.warn thread.backtrace.join("\n")
206
+ cli.logger.warn thread.backtrace.join("\n")
139
207
  else
140
- Sidekiq.logger.warn "<no backtrace available>"
208
+ cli.logger.warn "<no backtrace available>"
141
209
  end
142
210
  end
143
- end
211
+ }
212
+ }
213
+ UNHANDLED_SIGNAL_HANDLER = ->(cli) { cli.logger.info "No signal handler registered, ignoring" }
214
+ SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
215
+
216
+ def handle_signal(sig)
217
+ logger.debug "Got #{sig} signal"
218
+ SIGNAL_HANDLERS[sig].call(self)
144
219
  end
145
220
 
146
221
  private
147
222
 
148
223
  def print_banner
149
- # Print logo and banner for development
150
- if environment == 'development' && $stdout.tty?
151
- puts "\e[#{31}m"
152
- puts Sidekiq::CLI.banner
153
- puts "\e[0m"
154
- end
224
+ puts "\e[31m"
225
+ puts Sidekiq::CLI.banner
226
+ puts "\e[0m"
155
227
  end
156
228
 
157
- def load_celluloid
158
- raise "Celluloid cannot be required until here, or it will break Sidekiq's daemonization" if defined?(::Celluloid) && options[:daemon]
159
-
160
- # Celluloid can't be loaded until after we've daemonized
161
- # because it spins up threads and creates locks which get
162
- # into a very bad state if forked.
163
- require 'celluloid/autostart'
164
- Celluloid.logger = (options[:verbose] ? Sidekiq.logger : nil)
165
-
166
- require 'sidekiq/manager'
167
- require 'sidekiq/scheduled'
229
+ def set_environment(cli_env)
230
+ # See #984 for discussion.
231
+ # APP_ENV is now the preferred ENV term since it is not tech-specific.
232
+ # Both Sinatra 2.0+ and Sidekiq support this term.
233
+ # RAILS_ENV and RACK_ENV are there for legacy support.
234
+ @environment = cli_env || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
235
+ config[:environment] = @environment
168
236
  end
169
237
 
170
- def daemonize
171
- return unless options[:daemon]
172
-
173
- raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
174
- files_to_reopen = []
175
- ObjectSpace.each_object(File) do |file|
176
- files_to_reopen << file unless file.closed?
177
- end
178
-
179
- ::Process.daemon(true, true)
180
-
181
- files_to_reopen.each do |file|
182
- begin
183
- file.reopen file.path, "a+"
184
- file.sync = true
185
- rescue ::Exception
186
- end
187
- end
188
-
189
- [$stdout, $stderr].each do |io|
190
- File.open(options[:logfile], 'ab') do |f|
191
- io.reopen(f)
192
- end
193
- io.sync = true
238
+ def symbolize_keys_deep!(hash)
239
+ hash.keys.each do |k|
240
+ symkey = k.respond_to?(:to_sym) ? k.to_sym : k
241
+ hash[symkey] = hash.delete k
242
+ symbolize_keys_deep! hash[symkey] if hash[symkey].is_a? Hash
194
243
  end
195
- $stdin.reopen('/dev/null')
196
-
197
- initialize_logger
198
- end
199
-
200
- def set_environment(cli_env)
201
- @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
202
244
  end
203
245
 
204
246
  alias_method :die, :exit
205
247
  alias_method :☠, :exit
206
248
 
207
249
  def setup_options(args)
250
+ # parse CLI options
208
251
  opts = parse_options(args)
252
+
209
253
  set_environment opts[:environment]
210
254
 
211
- cfile = opts[:config_file]
212
- opts = parse_config(cfile).merge(opts) if cfile
255
+ # check config file presence
256
+ if opts[:config_file]
257
+ unless File.exist?(opts[:config_file])
258
+ raise ArgumentError, "No such file #{opts[:config_file]}"
259
+ end
260
+ else
261
+ config_dir = if File.directory?(opts[:require].to_s)
262
+ File.join(opts[:require], "config")
263
+ else
264
+ File.join(@config[:require], "config")
265
+ end
213
266
 
214
- opts[:strict] = true if opts[:strict].nil?
267
+ %w[sidekiq.yml sidekiq.yml.erb].each do |config_file|
268
+ path = File.join(config_dir, config_file)
269
+ opts[:config_file] ||= path if File.exist?(path)
270
+ end
271
+ end
215
272
 
216
- options.merge!(opts)
217
- end
273
+ # parse config file options
274
+ opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
218
275
 
219
- def options
220
- Sidekiq.options
221
- end
276
+ # set defaults
277
+ opts[:queues] = ["default"] if opts[:queues].nil?
278
+ opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
222
279
 
223
- def boot_system
224
- ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
280
+ # merge with defaults
281
+ @config.merge!(opts)
225
282
 
226
- raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
283
+ @config.default_capsule.tap do |cap|
284
+ cap.queues = opts[:queues]
285
+ cap.concurrency = opts[:concurrency] || @config[:concurrency]
286
+ end
227
287
 
228
- if File.directory?(options[:require])
229
- require 'rails'
230
- if ::Rails::VERSION::MAJOR < 4
231
- require 'sidekiq/rails'
232
- require File.expand_path("#{options[:require]}/config/environment.rb")
233
- ::Rails.application.eager_load!
234
- else
235
- # Painful contortions, see 1791 for discussion
236
- require File.expand_path("#{options[:require]}/config/application.rb")
237
- ::Rails::Application.initializer "sidekiq.eager_load" do
238
- ::Rails.application.config.eager_load = true
239
- end
240
- require 'sidekiq/rails'
241
- require File.expand_path("#{options[:require]}/config/environment.rb")
288
+ opts[:capsules]&.each do |name, cap_config|
289
+ @config.capsule(name.to_s) do |cap|
290
+ cap.queues = cap_config[:queues]
291
+ cap.concurrency = cap_config[:concurrency]
292
+ end
293
+ end
294
+ end
295
+
296
+ def boot_application
297
+ ENV["RACK_ENV"] = ENV["RAILS_ENV"] = environment
298
+
299
+ if File.directory?(@config[:require])
300
+ require "rails"
301
+ if ::Rails::VERSION::MAJOR < 6
302
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
242
303
  end
243
- options[:tag] ||= default_tag
304
+ require "sidekiq/rails"
305
+ require File.expand_path("#{@config[:require]}/config/environment.rb")
306
+ @config[:tag] ||= default_tag
244
307
  else
245
- require options[:require]
308
+ require @config[:require]
246
309
  end
247
310
  end
248
311
 
249
312
  def default_tag
250
313
  dir = ::Rails.root
251
314
  name = File.basename(dir)
252
- if name.to_i != 0 && prevdir = File.dirname(dir) # Capistrano release directory?
253
- if File.basename(prevdir) == 'releases'
315
+ prevdir = File.dirname(dir) # Capistrano release directory?
316
+ if name.to_i != 0 && prevdir
317
+ if File.basename(prevdir) == "releases"
254
318
  return File.basename(File.dirname(prevdir))
255
319
  end
256
320
  end
@@ -258,57 +322,52 @@ module Sidekiq
258
322
  end
259
323
 
260
324
  def validate!
261
- options[:queues] << 'default' if options[:queues].empty?
262
-
263
- if !File.exist?(options[:require]) ||
264
- (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
325
+ if !File.exist?(@config[:require]) ||
326
+ (File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
265
327
  logger.info "=================================================================="
266
- logger.info " Please point sidekiq to a Rails 3/4 application or a Ruby file "
267
- logger.info " to load your worker classes with -r [DIR|FILE]."
328
+ logger.info " Please point Sidekiq to a Rails application or a Ruby file "
329
+ logger.info " to load your job classes with -r [DIR|FILE]."
268
330
  logger.info "=================================================================="
269
331
  logger.info @parser
270
332
  die(1)
271
333
  end
272
334
 
273
335
  [:concurrency, :timeout].each do |opt|
274
- raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.has_key?(opt) && options[opt].to_i <= 0
336
+ raise ArgumentError, "#{opt}: #{@config[opt]} is not a valid value" if @config[opt].to_i <= 0
275
337
  end
276
338
  end
277
339
 
278
340
  def parse_options(argv)
279
341
  opts = {}
342
+ @parser = option_parser(opts)
343
+ @parser.parse!(argv)
344
+ opts
345
+ end
280
346
 
281
- @parser = OptionParser.new do |o|
282
- o.on '-c', '--concurrency INT', "processor threads to use" do |arg|
347
+ def option_parser(opts)
348
+ parser = OptionParser.new { |o|
349
+ o.on "-c", "--concurrency INT", "processor threads to use" do |arg|
283
350
  opts[:concurrency] = Integer(arg)
284
351
  end
285
352
 
286
- o.on '-d', '--daemon', "Daemonize process" do |arg|
287
- opts[:daemon] = arg
288
- end
289
-
290
- o.on '-e', '--environment ENV', "Application environment" do |arg|
353
+ o.on "-e", "--environment ENV", "Application environment" do |arg|
291
354
  opts[:environment] = arg
292
355
  end
293
356
 
294
- o.on '-g', '--tag TAG', "Process tag for procline" do |arg|
357
+ o.on "-g", "--tag TAG", "Process tag for procline" do |arg|
295
358
  opts[:tag] = arg
296
359
  end
297
360
 
298
- o.on '-i', '--index INT', "unique process index on this machine" do |arg|
299
- opts[:index] = Integer(arg.match(/\d+/)[0])
300
- end
301
-
302
361
  o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
303
- queue, weight = arg.split(",")
304
- parse_queue opts, queue, weight
362
+ opts[:queues] ||= []
363
+ opts[:queues] << arg
305
364
  end
306
365
 
307
- o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
366
+ o.on "-r", "--require [PATH|DIR]", "Location of Rails application with jobs or file to require" do |arg|
308
367
  opts[:require] = arg
309
368
  end
310
369
 
311
- o.on '-t', '--timeout NUM', "Shutdown timeout" do |arg|
370
+ o.on "-t", "--timeout NUM", "Shutdown timeout" do |arg|
312
371
  opts[:timeout] = Integer(arg)
313
372
  end
314
373
 
@@ -316,77 +375,52 @@ module Sidekiq
316
375
  opts[:verbose] = arg
317
376
  end
318
377
 
319
- o.on '-C', '--config PATH', "path to YAML config file" do |arg|
378
+ o.on "-C", "--config PATH", "path to YAML config file" do |arg|
320
379
  opts[:config_file] = arg
321
380
  end
322
381
 
323
- o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
324
- opts[:logfile] = arg
325
- end
326
-
327
- o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
328
- opts[:pidfile] = arg
329
- end
330
-
331
- o.on '-V', '--version', "Print version and exit" do |arg|
382
+ o.on "-V", "--version", "Print version and exit" do
332
383
  puts "Sidekiq #{Sidekiq::VERSION}"
333
384
  die(0)
334
385
  end
335
- end
386
+ }
336
387
 
337
- @parser.banner = "sidekiq [options]"
338
- @parser.on_tail "-h", "--help", "Show help" do
339
- logger.info @parser
388
+ parser.banner = "sidekiq [options]"
389
+ parser.on_tail "-h", "--help", "Show help" do
390
+ logger.info parser
340
391
  die 1
341
392
  end
342
- @parser.parse!(argv)
343
- opts[:config_file] ||= 'config/sidekiq.yml' if File.exist?('config/sidekiq.yml')
344
- opts
393
+
394
+ parser
345
395
  end
346
396
 
347
397
  def initialize_logger
348
- Sidekiq::Logging.initialize_logger(options[:logfile]) if options[:logfile]
349
-
350
- Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
398
+ @config.logger.level = ::Logger::DEBUG if @config[:verbose]
351
399
  end
352
400
 
353
- def write_pid
354
- if path = options[:pidfile]
355
- pidfile = File.expand_path(path)
356
- File.open(pidfile, 'w') do |f|
357
- f.puts ::Process.pid
358
- end
359
- end
360
- end
401
+ def parse_config(path)
402
+ erb = ERB.new(File.read(path), trim_mode: "-")
403
+ erb.filename = File.expand_path(path)
404
+ opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
361
405
 
362
- def parse_config(cfile)
363
- opts = {}
364
- if File.exist?(cfile)
365
- opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
366
- opts = opts.merge(opts.delete(environment) || {})
367
- parse_queues(opts, opts.delete(:queues) || [])
406
+ if opts.respond_to? :deep_symbolize_keys!
407
+ opts.deep_symbolize_keys!
368
408
  else
369
- # allow a non-existent config file so Sidekiq
370
- # can be deployed by cap with just the defaults.
371
- end
372
- ns = opts.delete(:namespace)
373
- if ns
374
- # logger hasn't been initialized yet, puts is all we have.
375
- puts("namespace should be set in your ruby initializer, is ignored in config file")
376
- puts("config.redis = { :url => ..., :namespace => '#{ns}' }")
409
+ symbolize_keys_deep!(opts)
377
410
  end
378
- opts
379
- end
380
411
 
381
- def parse_queues(opts, queues_and_weights)
382
- queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
412
+ opts = opts.merge(opts.delete(environment.to_sym) || {})
413
+ opts.delete(:strict)
414
+
415
+ opts
383
416
  end
384
417
 
385
- def parse_queue(opts, q, weight=nil)
386
- [weight.to_i, 1].max.times do
387
- (opts[:queues] ||= []) << q
388
- end
389
- opts[:strict] = false if weight.to_i > 0
418
+ def rails_app?
419
+ defined?(::Rails) && ::Rails.respond_to?(:application)
390
420
  end
391
421
  end
392
422
  end
423
+
424
+ require "sidekiq/systemd"
425
+ require "sidekiq/metrics/tracking"
426
+ require "sidekiq/job/interrupt_handler"