sidekiq 4.2.10 → 7.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +859 -7
  3. data/LICENSE.txt +9 -0
  4. data/README.md +49 -50
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +212 -119
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/generators/sidekiq/job_generator.rb +59 -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/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  13. data/lib/sidekiq/api.rb +680 -315
  14. data/lib/sidekiq/capsule.rb +132 -0
  15. data/lib/sidekiq/cli.rb +268 -248
  16. data/lib/sidekiq/client.rb +136 -101
  17. data/lib/sidekiq/component.rb +68 -0
  18. data/lib/sidekiq/config.rb +293 -0
  19. data/lib/sidekiq/deploy.rb +64 -0
  20. data/lib/sidekiq/embedded.rb +63 -0
  21. data/lib/sidekiq/fetch.rb +49 -42
  22. data/lib/sidekiq/iterable_job.rb +55 -0
  23. data/lib/sidekiq/job/interrupt_handler.rb +24 -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 +62 -0
  30. data/lib/sidekiq/job_retry.rb +305 -0
  31. data/lib/sidekiq/job_util.rb +109 -0
  32. data/lib/sidekiq/launcher.rb +208 -108
  33. data/lib/sidekiq/logger.rb +131 -0
  34. data/lib/sidekiq/manager.rb +43 -47
  35. data/lib/sidekiq/metrics/query.rb +158 -0
  36. data/lib/sidekiq/metrics/shared.rb +97 -0
  37. data/lib/sidekiq/metrics/tracking.rb +148 -0
  38. data/lib/sidekiq/middleware/chain.rb +113 -56
  39. data/lib/sidekiq/middleware/current_attributes.rb +113 -0
  40. data/lib/sidekiq/middleware/i18n.rb +7 -7
  41. data/lib/sidekiq/middleware/modules.rb +23 -0
  42. data/lib/sidekiq/monitor.rb +147 -0
  43. data/lib/sidekiq/paginator.rb +28 -16
  44. data/lib/sidekiq/processor.rb +188 -98
  45. data/lib/sidekiq/rails.rb +46 -97
  46. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  47. data/lib/sidekiq/redis_connection.rb +71 -73
  48. data/lib/sidekiq/ring_buffer.rb +31 -0
  49. data/lib/sidekiq/scheduled.rb +140 -51
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +26 -0
  52. data/lib/sidekiq/testing/inline.rb +6 -5
  53. data/lib/sidekiq/testing.rb +95 -85
  54. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  55. data/lib/sidekiq/version.rb +3 -1
  56. data/lib/sidekiq/web/action.rb +22 -16
  57. data/lib/sidekiq/web/application.rb +230 -86
  58. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  59. data/lib/sidekiq/web/helpers.rb +241 -104
  60. data/lib/sidekiq/web/router.rb +23 -19
  61. data/lib/sidekiq/web.rb +118 -110
  62. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  63. data/lib/sidekiq.rb +96 -185
  64. data/sidekiq.gemspec +26 -27
  65. data/web/assets/images/apple-touch-icon.png +0 -0
  66. data/web/assets/javascripts/application.js +157 -61
  67. data/web/assets/javascripts/base-charts.js +106 -0
  68. data/web/assets/javascripts/chart.min.js +13 -0
  69. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  70. data/web/assets/javascripts/dashboard-charts.js +192 -0
  71. data/web/assets/javascripts/dashboard.js +37 -280
  72. data/web/assets/javascripts/metrics.js +298 -0
  73. data/web/assets/stylesheets/application-dark.css +147 -0
  74. data/web/assets/stylesheets/application-rtl.css +163 -0
  75. data/web/assets/stylesheets/application.css +173 -198
  76. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  77. data/web/assets/stylesheets/bootstrap.css +2 -2
  78. data/web/locales/ar.yml +87 -0
  79. data/web/locales/cs.yml +62 -62
  80. data/web/locales/da.yml +60 -53
  81. data/web/locales/de.yml +65 -53
  82. data/web/locales/el.yml +43 -24
  83. data/web/locales/en.yml +86 -64
  84. data/web/locales/es.yml +70 -53
  85. data/web/locales/fa.yml +65 -64
  86. data/web/locales/fr.yml +83 -62
  87. data/web/locales/gd.yml +99 -0
  88. data/web/locales/he.yml +80 -0
  89. data/web/locales/hi.yml +59 -59
  90. data/web/locales/it.yml +53 -53
  91. data/web/locales/ja.yml +75 -62
  92. data/web/locales/ko.yml +52 -52
  93. data/web/locales/lt.yml +83 -0
  94. data/web/locales/nb.yml +61 -61
  95. data/web/locales/nl.yml +52 -52
  96. data/web/locales/pl.yml +45 -45
  97. data/web/locales/pt-br.yml +83 -55
  98. data/web/locales/pt.yml +51 -51
  99. data/web/locales/ru.yml +68 -63
  100. data/web/locales/sv.yml +53 -53
  101. data/web/locales/ta.yml +60 -60
  102. data/web/locales/tr.yml +101 -0
  103. data/web/locales/uk.yml +62 -61
  104. data/web/locales/ur.yml +80 -0
  105. data/web/locales/vi.yml +83 -0
  106. data/web/locales/zh-cn.yml +43 -16
  107. data/web/locales/zh-tw.yml +42 -8
  108. data/web/views/_footer.erb +21 -3
  109. data/web/views/_job_info.erb +21 -4
  110. data/web/views/_metrics_period_select.erb +12 -0
  111. data/web/views/_nav.erb +5 -19
  112. data/web/views/_paging.erb +3 -1
  113. data/web/views/_poll_link.erb +3 -6
  114. data/web/views/_summary.erb +7 -7
  115. data/web/views/busy.erb +85 -31
  116. data/web/views/dashboard.erb +50 -20
  117. data/web/views/dead.erb +3 -3
  118. data/web/views/filtering.erb +7 -0
  119. data/web/views/layout.erb +17 -6
  120. data/web/views/metrics.erb +91 -0
  121. data/web/views/metrics_for_job.erb +59 -0
  122. data/web/views/morgue.erb +14 -15
  123. data/web/views/queue.erb +34 -24
  124. data/web/views/queues.erb +20 -4
  125. data/web/views/retries.erb +19 -16
  126. data/web/views/retry.erb +3 -3
  127. data/web/views/scheduled.erb +19 -17
  128. metadata +91 -198
  129. data/.github/contributing.md +0 -32
  130. data/.github/issue_template.md +0 -9
  131. data/.gitignore +0 -12
  132. data/.travis.yml +0 -18
  133. data/3.0-Upgrade.md +0 -70
  134. data/4.0-Upgrade.md +0 -53
  135. data/COMM-LICENSE +0 -95
  136. data/Ent-Changes.md +0 -173
  137. data/Gemfile +0 -29
  138. data/LICENSE +0 -9
  139. data/Pro-2.0-Upgrade.md +0 -138
  140. data/Pro-3.0-Upgrade.md +0 -44
  141. data/Pro-Changes.md +0 -628
  142. data/Rakefile +0 -12
  143. data/bin/sidekiqctl +0 -99
  144. data/code_of_conduct.md +0 -50
  145. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  146. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  147. data/lib/sidekiq/core_ext.rb +0 -119
  148. data/lib/sidekiq/exception_handler.rb +0 -31
  149. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  150. data/lib/sidekiq/extensions/active_record.rb +0 -40
  151. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  152. data/lib/sidekiq/extensions/generic_proxy.rb +0 -25
  153. data/lib/sidekiq/logging.rb +0 -106
  154. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  155. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  156. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  157. data/lib/sidekiq/util.rb +0 -63
  158. data/lib/sidekiq/worker.rb +0 -121
data/lib/sidekiq/cli.rb CHANGED
@@ -1,266 +1,320 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
2
+
3
3
  $stdout.sync = true
4
4
 
5
- require 'yaml'
6
- require 'singleton'
7
- require 'optparse'
8
- require 'erb'
9
- require 'fileutils'
5
+ require "yaml"
6
+ require "singleton"
7
+ require "optparse"
8
+ require "erb"
9
+ require "fileutils"
10
10
 
11
- require 'sidekiq'
12
- require 'sidekiq/util'
11
+ require "sidekiq"
12
+ require "sidekiq/config"
13
+ require "sidekiq/component"
14
+ require "sidekiq/capsule"
15
+ require "sidekiq/launcher"
13
16
 
14
- module Sidekiq
17
+ module Sidekiq # :nodoc:
15
18
  class CLI
16
- include Util
19
+ include Sidekiq::Component
17
20
  include Singleton unless $TESTING
18
21
 
19
- PROCTITLES = [
20
- proc { 'sidekiq'.freeze },
21
- proc { Sidekiq::VERSION },
22
- proc { |me, data| data['tag'] },
23
- proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data['concurrency']} busy]" },
24
- proc { |me, data| "stopping" if me.stopping? },
25
- ]
26
-
27
- # Used for CLI testing
28
- attr_accessor :code
29
22
  attr_accessor :launcher
30
23
  attr_accessor :environment
24
+ attr_accessor :config
31
25
 
32
- def initialize
33
- @code = nil
34
- end
35
-
36
- def parse(args=ARGV)
37
- @code = nil
26
+ def parse(args = ARGV.dup)
27
+ @config ||= Sidekiq.default_configuration
38
28
 
39
29
  setup_options(args)
40
30
  initialize_logger
41
31
  validate!
42
- daemonize
43
- write_pid
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 TSTP).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" unless defined?(::Sidekiq::Pro)
71
+ logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
68
72
 
69
73
  # touch the connection pool so it is created before we
70
74
  # fire startup and start multithreading.
71
- ver = Sidekiq.redis_info['redis_version']
72
- raise "You are using Redis v#{ver}, Sidekiq requires Redis v2.8.0 or greater" if ver < '2.8'
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
88
+
89
+ EOM
90
+ end
91
+
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
96
+ end
97
+
98
+ # cache process identity
99
+ @config[:identity] = identity
73
100
 
74
101
  # Touch middleware so it isn't lazy loaded by multiple threads, #3043
75
- Sidekiq.server_middleware
102
+ @config.server_middleware
103
+
104
+ ::Process.warmup if warmup && ::Process.respond_to?(:warmup)
76
105
 
77
106
  # Before this point, the process is initializing with just the main thread.
78
107
  # Starting here the process will now have multiple threads running.
79
- fire_event(:startup)
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(", ")}" }
80
112
 
81
- logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(', ')}" }
82
- logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(', ')}" }
113
+ launch(self_read)
114
+ end
83
115
 
84
- if !options[:daemon]
85
- logger.info 'Starting processing, hit Ctrl-C to stop'
116
+ def launch(self_read)
117
+ if environment == "development" && $stdout.tty?
118
+ logger.info "Starting processing, hit Ctrl-C to stop"
86
119
  end
87
120
 
88
- require 'sidekiq/launcher'
89
- @launcher = Sidekiq::Launcher.new(options)
121
+ @launcher = Sidekiq::Launcher.new(@config)
90
122
 
91
123
  begin
92
124
  launcher.run
93
125
 
94
- while readable_io = IO.select([self_read])
95
- signal = readable_io.first[0].gets.strip
126
+ while self_read.wait_readable
127
+ signal = self_read.gets.strip
96
128
  handle_signal(signal)
97
129
  end
98
130
  rescue Interrupt
99
- logger.info 'Shutting down'
131
+ logger.info "Shutting down"
100
132
  launcher.stop
101
- # Explicitly exit so busy Processor threads can't block
102
- # process shutdown.
103
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.
104
140
  exit(0)
105
141
  end
106
142
  end
107
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
+
108
175
  def self.banner
109
- %q{
110
- m,
111
- `$b
112
- .ss, $$: .,d$
113
- `$$P,d$P' .,md$P"'
114
- ,$$$$$bmmd$$$P^'
115
- .d$$$$$$$$$$P'
116
- $$^' `"^$$$' ____ _ _ _ _
117
- $: ,$$: / ___|(_) __| | ___| | _(_) __ _
118
- `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` |
119
- $$: ___) | | (_| | __/ <| | (_| |
120
- $$ |____/|_|\__,_|\___|_|\_\_|\__, |
121
- .d$$ |_|
122
- }
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}}
123
190
  end
124
191
 
125
- def handle_signal(sig)
126
- Sidekiq.logger.debug "Got #{sig} signal"
127
- case sig
128
- when 'INT'
129
- # Handle Ctrl-C in JRuby like MRI
130
- # http://jira.codehaus.org/browse/JRUBY-4637
131
- raise Interrupt
132
- when 'TERM'
133
- # Heroku sends TERM and then waits 10 seconds for process to exit.
134
- raise Interrupt
135
- when 'USR1'
136
- Sidekiq.logger.info "Received USR1, no longer accepting new work"
137
- launcher.quiet
138
- when 'TSTP'
139
- # USR1 is not available on JVM, allow TSTP as an alternate signal
140
- Sidekiq.logger.info "Received TSTP, no longer accepting new work"
141
- launcher.quiet
142
- when 'USR2'
143
- if Sidekiq.options[:logfile]
144
- Sidekiq.logger.info "Received USR2, reopening log file"
145
- Sidekiq::Logging.reopen_logs
146
- end
147
- 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) {
148
203
  Thread.list.each do |thread|
149
- Sidekiq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['sidekiq_label']}"
204
+ cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
150
205
  if thread.backtrace
151
- Sidekiq.logger.warn thread.backtrace.join("\n")
206
+ cli.logger.warn thread.backtrace.join("\n")
152
207
  else
153
- Sidekiq.logger.warn "<no backtrace available>"
208
+ cli.logger.warn "<no backtrace available>"
154
209
  end
155
210
  end
156
- 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)
157
219
  end
158
220
 
159
221
  private
160
222
 
161
223
  def print_banner
162
- # Print logo and banner for development
163
- if environment == 'development' && $stdout.tty?
164
- puts "\e[#{31}m"
165
- puts Sidekiq::CLI.banner
166
- puts "\e[0m"
167
- end
224
+ puts "\e[31m"
225
+ puts Sidekiq::CLI.banner
226
+ puts "\e[0m"
168
227
  end
169
228
 
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
194
- end
195
- $stdin.reopen('/dev/null')
196
-
197
- initialize_logger
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
198
236
  end
199
237
 
200
- def set_environment(cli_env)
201
- @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
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
243
+ end
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?
215
- opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if !opts[:concurrency] && ENV["RAILS_MAX_THREADS"]
216
- opts[:identity] = identity
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
217
272
 
218
- options.merge!(opts)
219
- end
273
+ # parse config file options
274
+ opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
220
275
 
221
- def options
222
- Sidekiq.options
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"]
279
+
280
+ # merge with defaults
281
+ @config.merge!(opts)
282
+
283
+ @config.default_capsule.tap do |cap|
284
+ cap.queues = opts[:queues]
285
+ cap.concurrency = opts[:concurrency] || @config[:concurrency]
286
+ end
287
+
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
223
294
  end
224
295
 
225
- def boot_system
226
- ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
227
-
228
- raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
229
-
230
- if File.directory?(options[:require])
231
- require 'rails'
232
- if ::Rails::VERSION::MAJOR < 4
233
- require 'sidekiq/rails'
234
- require File.expand_path("#{options[:require]}/config/environment.rb")
235
- ::Rails.application.eager_load!
236
- elsif ::Rails::VERSION::MAJOR == 4
237
- # Painful contortions, see 1791 for discussion
238
- # No autoloading, we want to force eager load for everything.
239
- require File.expand_path("#{options[:require]}/config/application.rb")
240
- ::Rails::Application.initializer "sidekiq.eager_load" do
241
- ::Rails.application.config.eager_load = true
242
- end
243
- require 'sidekiq/rails'
244
- require File.expand_path("#{options[:require]}/config/environment.rb")
245
- else
246
- # Rails 5+ && development mode, use Reloader
247
- require 'sidekiq/rails'
248
- require File.expand_path("#{options[:require]}/config/environment.rb")
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+"
249
303
  end
250
- options[:tag] ||= default_tag
304
+ require "sidekiq/rails"
305
+ require File.expand_path("#{@config[:require]}/config/environment.rb")
306
+ @config[:tag] ||= default_tag
251
307
  else
252
- not_required_message = "#{options[:require]} was not required, you should use an explicit path: " +
253
- "./#{options[:require]} or /path/to/#{options[:require]}"
254
-
255
- require(options[:require]) || raise(ArgumentError, not_required_message)
308
+ require @config[:require]
256
309
  end
257
310
  end
258
311
 
259
312
  def default_tag
260
313
  dir = ::Rails.root
261
314
  name = File.basename(dir)
262
- if name.to_i != 0 && prevdir = File.dirname(dir) # Capistrano release directory?
263
- 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"
264
318
  return File.basename(File.dirname(prevdir))
265
319
  end
266
320
  end
@@ -268,57 +322,52 @@ module Sidekiq
268
322
  end
269
323
 
270
324
  def validate!
271
- options[:queues] << 'default' if options[:queues].empty?
272
-
273
- if !File.exist?(options[:require]) ||
274
- (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"))
275
327
  logger.info "=================================================================="
276
- logger.info " Please point sidekiq to a Rails 3/4 application or a Ruby file "
277
- 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]."
278
330
  logger.info "=================================================================="
279
331
  logger.info @parser
280
332
  die(1)
281
333
  end
282
334
 
283
335
  [:concurrency, :timeout].each do |opt|
284
- 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
285
337
  end
286
338
  end
287
339
 
288
340
  def parse_options(argv)
289
341
  opts = {}
342
+ @parser = option_parser(opts)
343
+ @parser.parse!(argv)
344
+ opts
345
+ end
290
346
 
291
- @parser = OptionParser.new do |o|
292
- 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|
293
350
  opts[:concurrency] = Integer(arg)
294
351
  end
295
352
 
296
- o.on '-d', '--daemon', "Daemonize process" do |arg|
297
- opts[:daemon] = arg
298
- end
299
-
300
- o.on '-e', '--environment ENV', "Application environment" do |arg|
353
+ o.on "-e", "--environment ENV", "Application environment" do |arg|
301
354
  opts[:environment] = arg
302
355
  end
303
356
 
304
- o.on '-g', '--tag TAG', "Process tag for procline" do |arg|
357
+ o.on "-g", "--tag TAG", "Process tag for procline" do |arg|
305
358
  opts[:tag] = arg
306
359
  end
307
360
 
308
- o.on '-i', '--index INT', "unique process index on this machine" do |arg|
309
- opts[:index] = Integer(arg.match(/\d+/)[0])
310
- end
311
-
312
361
  o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
313
- queue, weight = arg.split(",")
314
- parse_queue opts, queue, weight
362
+ opts[:queues] ||= []
363
+ opts[:queues] << arg
315
364
  end
316
365
 
317
- 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|
318
367
  opts[:require] = arg
319
368
  end
320
369
 
321
- o.on '-t', '--timeout NUM', "Shutdown timeout" do |arg|
370
+ o.on "-t", "--timeout NUM", "Shutdown timeout" do |arg|
322
371
  opts[:timeout] = Integer(arg)
323
372
  end
324
373
 
@@ -326,81 +375,52 @@ module Sidekiq
326
375
  opts[:verbose] = arg
327
376
  end
328
377
 
329
- 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|
330
379
  opts[:config_file] = arg
331
380
  end
332
381
 
333
- o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
334
- opts[:logfile] = arg
335
- end
336
-
337
- o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
338
- opts[:pidfile] = arg
339
- end
340
-
341
- o.on '-V', '--version', "Print version and exit" do |arg|
382
+ o.on "-V", "--version", "Print version and exit" do
342
383
  puts "Sidekiq #{Sidekiq::VERSION}"
343
384
  die(0)
344
385
  end
345
- end
386
+ }
346
387
 
347
- @parser.banner = "sidekiq [options]"
348
- @parser.on_tail "-h", "--help", "Show help" do
349
- logger.info @parser
388
+ parser.banner = "sidekiq [options]"
389
+ parser.on_tail "-h", "--help", "Show help" do
390
+ logger.info parser
350
391
  die 1
351
392
  end
352
- @parser.parse!(argv)
353
-
354
- %w[config/sidekiq.yml config/sidekiq.yml.erb].each do |filename|
355
- opts[:config_file] ||= filename if File.exist?(filename)
356
- end
357
393
 
358
- opts
394
+ parser
359
395
  end
360
396
 
361
397
  def initialize_logger
362
- Sidekiq::Logging.initialize_logger(options[:logfile]) if options[:logfile]
363
-
364
- Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose]
398
+ @config.logger.level = ::Logger::DEBUG if @config[:verbose]
365
399
  end
366
400
 
367
- def write_pid
368
- if path = options[:pidfile]
369
- pidfile = File.expand_path(path)
370
- File.open(pidfile, 'w') do |f|
371
- f.puts ::Process.pid
372
- end
373
- end
374
- 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) || {}
375
405
 
376
- def parse_config(cfile)
377
- opts = {}
378
- if File.exist?(cfile)
379
- opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
380
- opts = opts.merge(opts.delete(environment) || {})
381
- parse_queues(opts, opts.delete(:queues) || [])
406
+ if opts.respond_to? :deep_symbolize_keys!
407
+ opts.deep_symbolize_keys!
382
408
  else
383
- # allow a non-existent config file so Sidekiq
384
- # can be deployed by cap with just the defaults.
385
- end
386
- ns = opts.delete(:namespace)
387
- if ns
388
- # logger hasn't been initialized yet, puts is all we have.
389
- puts("namespace should be set in your ruby initializer, is ignored in config file")
390
- puts("config.redis = { :url => ..., :namespace => '#{ns}' }")
409
+ symbolize_keys_deep!(opts)
391
410
  end
392
- opts
393
- end
394
411
 
395
- def parse_queues(opts, queues_and_weights)
396
- 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
397
416
  end
398
417
 
399
- def parse_queue(opts, q, weight=nil)
400
- [weight.to_i, 1].max.times do
401
- (opts[:queues] ||= []) << q
402
- end
403
- opts[:strict] = false if weight.to_i > 0
418
+ def rails_app?
419
+ defined?(::Rails) && ::Rails.respond_to?(:application)
404
420
  end
405
421
  end
406
422
  end
423
+
424
+ require "sidekiq/systemd"
425
+ require "sidekiq/metrics/tracking"
426
+ require "sidekiq/job/interrupt_handler"