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
@@ -0,0 +1,291 @@
1
+ require "forwardable"
2
+
3
+ require "set"
4
+ require "sidekiq/redis_connection"
5
+
6
+ module Sidekiq
7
+ # Sidekiq::Config represents the global configuration for an instance of Sidekiq.
8
+ class Config
9
+ extend Forwardable
10
+
11
+ DEFAULTS = {
12
+ labels: Set.new,
13
+ require: ".",
14
+ environment: nil,
15
+ concurrency: 5,
16
+ timeout: 25,
17
+ poll_interval_average: nil,
18
+ average_scheduled_poll_interval: 5,
19
+ on_complex_arguments: :raise,
20
+ iteration: {
21
+ max_job_runtime: nil,
22
+ retry_backoff: 0
23
+ },
24
+ error_handlers: [],
25
+ death_handlers: [],
26
+ lifecycle_events: {
27
+ startup: [],
28
+ quiet: [],
29
+ shutdown: [],
30
+ # triggers when we fire the first heartbeat on startup OR repairing a network partition
31
+ heartbeat: [],
32
+ # triggers on EVERY heartbeat call, every 10 seconds
33
+ beat: []
34
+ },
35
+ dead_max_jobs: 10_000,
36
+ dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
37
+ reloader: proc { |&block| block.call },
38
+ backtrace_cleaner: ->(backtrace) { backtrace }
39
+ }
40
+
41
+ ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
42
+ l = cfg.logger
43
+ l.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
44
+ l.warn("#{ex.class.name}: #{ex.message}")
45
+ unless ex.backtrace.nil?
46
+ backtrace = cfg[:backtrace_cleaner].call(ex.backtrace)
47
+ l.warn(backtrace.join("\n"))
48
+ end
49
+ }
50
+
51
+ def initialize(options = {})
52
+ @options = DEFAULTS.merge(options)
53
+ @options[:error_handlers] << ERROR_HANDLER if @options[:error_handlers].empty?
54
+ @directory = {}
55
+ @redis_config = {}
56
+ @capsules = {}
57
+ end
58
+
59
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
60
+ attr_reader :capsules
61
+
62
+ def to_json(*)
63
+ Sidekiq.dump_json(@options)
64
+ end
65
+
66
+ # LEGACY: edits the default capsule
67
+ # config.concurrency = 5
68
+ def concurrency=(val)
69
+ default_capsule.concurrency = Integer(val)
70
+ end
71
+
72
+ def concurrency
73
+ default_capsule.concurrency
74
+ end
75
+
76
+ def total_concurrency
77
+ capsules.each_value.sum(&:concurrency)
78
+ end
79
+
80
+ # Edit the default capsule.
81
+ # config.queues = %w( high default low ) # strict
82
+ # config.queues = %w( high,3 default,2 low,1 ) # weighted
83
+ # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
84
+ #
85
+ # With weighted priority, queue will be checked first (weight / total) of the time.
86
+ # high will be checked first (3/6) or 50% of the time.
87
+ # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
88
+ # are ridiculous and unnecessarily expensive. You can get random queue ordering
89
+ # by explicitly setting all weights to 1.
90
+ def queues=(val)
91
+ default_capsule.queues = val
92
+ end
93
+
94
+ def queues
95
+ default_capsule.queues
96
+ end
97
+
98
+ def client_middleware
99
+ @client_chain ||= Sidekiq::Middleware::Chain.new(self)
100
+ yield @client_chain if block_given?
101
+ @client_chain
102
+ end
103
+
104
+ def server_middleware
105
+ @server_chain ||= Sidekiq::Middleware::Chain.new(self)
106
+ yield @server_chain if block_given?
107
+ @server_chain
108
+ end
109
+
110
+ def default_capsule(&block)
111
+ capsule("default", &block)
112
+ end
113
+
114
+ # register a new queue processing subsystem
115
+ def capsule(name)
116
+ nm = name.to_s
117
+ cap = @capsules.fetch(nm) do
118
+ cap = Sidekiq::Capsule.new(nm, self)
119
+ @capsules[nm] = cap
120
+ end
121
+ yield cap if block_given?
122
+ cap
123
+ end
124
+
125
+ # All capsules must use the same Redis configuration
126
+ def redis=(hash)
127
+ @redis_config = @redis_config.merge(hash)
128
+ end
129
+
130
+ def redis_pool
131
+ Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
132
+ end
133
+
134
+ private def local_redis_pool
135
+ # this is our internal client/housekeeping pool. each capsule has its
136
+ # own pool for executing threads.
137
+ @redis ||= new_redis_pool(10, "internal")
138
+ end
139
+
140
+ def new_redis_pool(size, name = "unset")
141
+ # connection pool is lazy, it will not create connections unless you actually need them
142
+ # so don't be skimpy!
143
+ RedisConnection.create({size: size, logger: logger, pool_name: name}.merge(@redis_config))
144
+ end
145
+
146
+ def redis_info
147
+ redis do |conn|
148
+ conn.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
149
+ rescue RedisClientAdapter::CommandError => ex
150
+ # 2850 return fake version when INFO command has (probably) been renamed
151
+ raise unless /unknown command/.match?(ex.message)
152
+ {
153
+ "redis_version" => "9.9.9",
154
+ "uptime_in_days" => "9999",
155
+ "connected_clients" => "9999",
156
+ "used_memory_human" => "9P",
157
+ "used_memory_peak_human" => "9P"
158
+ }.freeze
159
+ end
160
+ end
161
+
162
+ def redis
163
+ raise ArgumentError, "requires a block" unless block_given?
164
+ redis_pool.with do |conn|
165
+ retryable = true
166
+ begin
167
+ yield conn
168
+ rescue RedisClientAdapter::BaseError => ex
169
+ # 2550 Failover can cause the server to become a replica, need
170
+ # to disconnect and reopen the socket to get back to the primary.
171
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
172
+ # 4985 Use the same logic when a blocking command is force-unblocked
173
+ # The same retry logic is also used in client.rb
174
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
175
+ conn.close
176
+ retryable = false
177
+ retry
178
+ end
179
+ raise
180
+ end
181
+ end
182
+ end
183
+
184
+ # register global singletons which can be accessed elsewhere
185
+ def register(name, instance)
186
+ @directory[name] = instance
187
+ end
188
+
189
+ # find a singleton
190
+ def lookup(name, default_class = nil)
191
+ # JNDI is just a fancy name for a hash lookup
192
+ @directory.fetch(name) do |key|
193
+ return nil unless default_class
194
+ @directory[key] = default_class.new(self)
195
+ end
196
+ end
197
+
198
+ ##
199
+ # Death handlers are called when all retries for a job have been exhausted and
200
+ # the job dies. It's the notification to your application
201
+ # that this job will not succeed without manual intervention.
202
+ #
203
+ # Sidekiq.configure_server do |config|
204
+ # config.death_handlers << ->(job, ex) do
205
+ # end
206
+ # end
207
+ def death_handlers
208
+ @options[:death_handlers]
209
+ end
210
+
211
+ # How frequently Redis should be checked by a random Sidekiq process for
212
+ # scheduled and retriable jobs. Each individual process will take turns by
213
+ # waiting some multiple of this value.
214
+ #
215
+ # See sidekiq/scheduled.rb for an in-depth explanation of this value
216
+ def average_scheduled_poll_interval=(interval)
217
+ @options[:average_scheduled_poll_interval] = interval
218
+ end
219
+
220
+ # Register a proc to handle any error which occurs within the Sidekiq process.
221
+ #
222
+ # Sidekiq.configure_server do |config|
223
+ # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
224
+ # end
225
+ #
226
+ # The default error handler logs errors to @logger.
227
+ def error_handlers
228
+ @options[:error_handlers]
229
+ end
230
+
231
+ # Register a block to run at a point in the Sidekiq lifecycle.
232
+ # :startup, :quiet or :shutdown are valid events.
233
+ #
234
+ # Sidekiq.configure_server do |config|
235
+ # config.on(:shutdown) do
236
+ # puts "Goodbye cruel world!"
237
+ # end
238
+ # end
239
+ def on(event, &block)
240
+ raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
241
+ raise ArgumentError, "Invalid event name: #{event}" unless @options[:lifecycle_events].key?(event)
242
+ @options[:lifecycle_events][event] << block
243
+ end
244
+
245
+ def logger
246
+ @logger ||= Sidekiq::Logger.new($stdout, level: :info).tap do |log|
247
+ log.level = Logger::INFO
248
+ log.formatter = if ENV["DYNO"]
249
+ Sidekiq::Logger::Formatters::WithoutTimestamp.new
250
+ else
251
+ Sidekiq::Logger::Formatters::Pretty.new
252
+ end
253
+ end
254
+ end
255
+
256
+ def logger=(logger)
257
+ if logger.nil?
258
+ self.logger.level = Logger::FATAL
259
+ return
260
+ end
261
+
262
+ @logger = logger
263
+ end
264
+
265
+ private def parameter_size(handler)
266
+ target = handler.is_a?(Proc) ? handler : handler.method(:call)
267
+ target.parameters.size
268
+ end
269
+
270
+ # INTERNAL USE ONLY
271
+ def handle_exception(ex, ctx = {})
272
+ if @options[:error_handlers].size == 0
273
+ p ["!!!!!", ex]
274
+ end
275
+ @options[:error_handlers].each do |handler|
276
+ if parameter_size(handler) == 2
277
+ # TODO Remove in 8.0
278
+ logger.info { "DEPRECATION: Sidekiq exception handlers now take three arguments, see #{handler}" }
279
+ handler.call(ex, {_config: self}.merge(ctx))
280
+ else
281
+ handler.call(ex, ctx, self)
282
+ end
283
+ rescue Exception => e
284
+ l = logger
285
+ l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
286
+ l.error e
287
+ l.error e.backtrace.join("\n") unless e.backtrace.nil?
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,62 @@
1
+ require "sidekiq/redis_connection"
2
+ require "time"
3
+
4
+ # This file is designed to be required within the user's
5
+ # deployment script; it should need a bare minimum of dependencies.
6
+ # Usage:
7
+ #
8
+ # require "sidekiq/deploy"
9
+ # Sidekiq::Deploy.mark!("Some change")
10
+ #
11
+ # If you do not pass a label, Sidekiq will try to use the latest
12
+ # git commit info.
13
+ #
14
+
15
+ module Sidekiq
16
+ class Deploy
17
+ MARK_TTL = 90 * 24 * 60 * 60 # 90 days
18
+
19
+ LABEL_MAKER = -> {
20
+ `git log -1 --format="%h %s"`.strip
21
+ }
22
+
23
+ def self.mark!(label = nil)
24
+ Sidekiq::Deploy.new.mark!(label: label)
25
+ end
26
+
27
+ def initialize(pool = Sidekiq::RedisConnection.create)
28
+ @pool = pool
29
+ end
30
+
31
+ def mark!(at: Time.now, label: nil)
32
+ label ||= LABEL_MAKER.call
33
+ # we need to round the timestamp so that we gracefully
34
+ # handle an very common error in marking deploys:
35
+ # having every process mark its deploy, leading
36
+ # to N marks for each deploy. Instead we round the time
37
+ # to the minute so that multiple marks within that minute
38
+ # will all naturally rollup into one mark per minute.
39
+ whence = at.utc
40
+ floor = Time.utc(whence.year, whence.month, whence.mday, whence.hour, whence.min, 0)
41
+ datecode = floor.strftime("%Y%m%d")
42
+ key = "#{datecode}-marks"
43
+ stamp = floor.iso8601
44
+
45
+ @pool.with do |c|
46
+ # only allow one deploy mark for a given label for the next minute
47
+ lock = c.set("deploylock-#{label}", stamp, "nx", "ex", "60")
48
+ if lock
49
+ c.multi do |pipe|
50
+ pipe.hsetnx(key, stamp, label)
51
+ pipe.expire(key, MARK_TTL)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def fetch(date = Time.now.utc.to_date)
58
+ datecode = date.strftime("%Y%m%d")
59
+ @pool.with { |c| c.hgetall("#{datecode}-marks") }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ require "sidekiq/component"
2
+ require "sidekiq/launcher"
3
+ require "sidekiq/metrics/tracking"
4
+
5
+ module Sidekiq
6
+ class Embedded
7
+ include Sidekiq::Component
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def run
14
+ housekeeping
15
+ fire_event(:startup, reverse: false, reraise: true)
16
+ @launcher = Sidekiq::Launcher.new(@config, embedded: true)
17
+ @launcher.run
18
+ sleep 0.2 # pause to give threads time to spin up
19
+
20
+ logger.info "Sidekiq running embedded, total process thread count: #{Thread.list.size}"
21
+ logger.debug { Thread.list.map(&:name) }
22
+ end
23
+
24
+ def quiet
25
+ @launcher&.quiet
26
+ end
27
+
28
+ def stop
29
+ @launcher&.stop
30
+ end
31
+
32
+ private
33
+
34
+ def housekeeping
35
+ logger.info "Running in #{RUBY_DESCRIPTION}"
36
+ logger.info Sidekiq::LICENSE
37
+ logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
38
+
39
+ # touch the connection pool so it is created before we
40
+ # fire startup and start multithreading.
41
+ info = config.redis_info
42
+ ver = Gem::Version.new(info["redis_version"])
43
+ raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
44
+
45
+ maxmemory_policy = info["maxmemory_policy"]
46
+ if maxmemory_policy != "noeviction"
47
+ logger.warn <<~EOM
48
+
49
+
50
+ WARNING: Your Redis instance will evict Sidekiq data under heavy load.
51
+ The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
52
+ See: https://github.com/sidekiq/sidekiq/wiki/Using-Redis#memory
53
+
54
+ EOM
55
+ end
56
+
57
+ logger.debug { "Client Middleware: #{@config.default_capsule.client_middleware.map(&:klass).join(", ")}" }
58
+ logger.debug { "Server Middleware: #{@config.default_capsule.server_middleware.map(&:klass).join(", ")}" }
59
+ end
60
+ end
61
+ end
data/lib/sidekiq/fetch.rb CHANGED
@@ -1,146 +1,73 @@
1
- require 'sidekiq'
2
- require 'sidekiq/util'
3
- require 'sidekiq/actor'
1
+ # frozen_string_literal: true
4
2
 
5
- module Sidekiq
6
- ##
7
- # The Fetcher blocks on Redis, waiting for a message to process
8
- # from the queues. It gets the message and hands it to the Manager
9
- # to assign to a ready Processor.
10
- class Fetcher
11
- include Util
12
- include Actor
3
+ require "sidekiq"
4
+ require "sidekiq/component"
5
+ require "sidekiq/capsule"
13
6
 
14
- TIMEOUT = 1
15
-
16
- attr_reader :down
17
-
18
- def initialize(mgr, options)
19
- @down = nil
20
- @mgr = mgr
21
- @strategy = Fetcher.strategy.new(options)
22
- end
23
-
24
- # Fetching is straightforward: the Manager makes a fetch
25
- # request for each idle processor when Sidekiq starts and
26
- # then issues a new fetch request every time a Processor
27
- # finishes a message.
28
- #
29
- # Because we have to shut down cleanly, we can't block
30
- # forever and we can't loop forever. Instead we reschedule
31
- # a new fetch if the current fetch turned up nothing.
32
- def fetch
33
- watchdog('Fetcher#fetch died') do
34
- return if Sidekiq::Fetcher.done?
35
-
36
- begin
37
- work = @strategy.retrieve_work
38
- ::Sidekiq.logger.info("Redis is online, #{Time.now - @down} sec downtime") if @down
39
- @down = nil
40
-
41
- if work
42
- @mgr.async.assign(work)
43
- else
44
- after(0) { fetch }
45
- end
46
- rescue => ex
47
- handle_fetch_exception(ex)
48
- end
7
+ module Sidekiq # :nodoc:
8
+ class BasicFetch
9
+ include Sidekiq::Component
10
+ # We want the fetch operation to timeout every few seconds so the thread
11
+ # can check if the process is shutting down.
12
+ TIMEOUT = 2
49
13
 
14
+ UnitOfWork = Struct.new(:queue, :job, :config) {
15
+ def acknowledge
16
+ # nothing to do
50
17
  end
51
- end
52
-
53
- private
54
18
 
55
- def pause
56
- sleep(TIMEOUT)
57
- end
19
+ def queue_name
20
+ queue.delete_prefix("queue:")
21
+ end
58
22
 
59
- def handle_fetch_exception(ex)
60
- if !@down
61
- logger.error("Error fetching message: #{ex}")
62
- ex.backtrace.each do |bt|
63
- logger.error(bt)
23
+ def requeue
24
+ config.redis do |conn|
25
+ conn.rpush(queue, job)
64
26
  end
65
27
  end
66
- @down ||= Time.now
67
- pause
68
- after(0) { fetch }
69
- rescue Task::TerminatedError
70
- # If redis is down when we try to shut down, all the fetch backlog
71
- # raises these errors. Haven't been able to figure out what I'm doing wrong.
72
- end
73
-
74
- # Ugh. Say hello to a bloody hack.
75
- # Can't find a clean way to get the fetcher to just stop processing
76
- # its mailbox when shutdown starts.
77
- def self.done!
78
- @done = true
79
- end
80
-
81
- def self.reset # testing only
82
- @done = nil
83
- end
84
-
85
- def self.done?
86
- @done
87
- end
88
-
89
- def self.strategy
90
- Sidekiq.options[:fetch] || BasicFetch
91
- end
92
- end
93
-
94
- class BasicFetch
95
- def initialize(options)
96
- @strictly_ordered_queues = !!options[:strict]
97
- @queues = options[:queues].map { |q| "queue:#{q}" }
98
- @unique_queues = @queues.uniq
28
+ }
29
+
30
+ def initialize(cap)
31
+ raise ArgumentError, "missing queue list" unless cap.queues
32
+ @config = cap
33
+ @strictly_ordered_queues = cap.mode == :strict
34
+ @queues = config.queues.map { |q| "queue:#{q}" }
35
+ @queues.uniq! if @strictly_ordered_queues
99
36
  end
100
37
 
101
38
  def retrieve_work
102
- work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd) }
103
- UnitOfWork.new(*work) if work
39
+ qs = queues_cmd
40
+ # 4825 Sidekiq Pro with all queues paused will return an
41
+ # empty set of queues
42
+ if qs.size <= 0
43
+ sleep(TIMEOUT)
44
+ return nil
45
+ end
46
+
47
+ queue, job = redis { |conn| conn.blocking_call(TIMEOUT, "brpop", *qs, TIMEOUT) }
48
+ UnitOfWork.new(queue, job, config) if queue
104
49
  end
105
50
 
106
- # By leaving this as a class method, it can be pluggable and used by the Manager actor. Making it
107
- # an instance method will make it async to the Fetcher actor
108
- def self.bulk_requeue(inprogress, options)
51
+ def bulk_requeue(inprogress)
109
52
  return if inprogress.empty?
110
53
 
111
- Sidekiq.logger.debug { "Re-queueing terminated jobs" }
54
+ logger.debug { "Re-queueing terminated jobs" }
112
55
  jobs_to_requeue = {}
113
56
  inprogress.each do |unit_of_work|
114
- jobs_to_requeue[unit_of_work.queue_name] ||= []
115
- jobs_to_requeue[unit_of_work.queue_name] << unit_of_work.message
57
+ jobs_to_requeue[unit_of_work.queue] ||= []
58
+ jobs_to_requeue[unit_of_work.queue] << unit_of_work.job
116
59
  end
117
60
 
118
- Sidekiq.redis do |conn|
119
- conn.pipelined do
61
+ redis do |conn|
62
+ conn.pipelined do |pipeline|
120
63
  jobs_to_requeue.each do |queue, jobs|
121
- conn.rpush("queue:#{queue}", jobs)
64
+ pipeline.rpush(queue, jobs)
122
65
  end
123
66
  end
124
67
  end
125
- Sidekiq.logger.info("Pushed #{inprogress.size} messages back to Redis")
68
+ logger.info("Pushed #{inprogress.size} jobs back to Redis")
126
69
  rescue => ex
127
- Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
128
- end
129
-
130
- UnitOfWork = Struct.new(:queue, :message) do
131
- def acknowledge
132
- # nothing to do
133
- end
134
-
135
- def queue_name
136
- queue.gsub(/.*queue:/, '')
137
- end
138
-
139
- def requeue
140
- Sidekiq.redis do |conn|
141
- conn.rpush("queue:#{queue_name}", message)
142
- end
143
- end
70
+ logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
144
71
  end
145
72
 
146
73
  # Creating the Redis#brpop command takes into account any
@@ -149,8 +76,13 @@ module Sidekiq
149
76
  # recreate the queue command each time we invoke Redis#brpop
150
77
  # to honor weights and avoid queue starvation.
151
78
  def queues_cmd
152
- queues = @strictly_ordered_queues ? @unique_queues.dup : @queues.shuffle.uniq
153
- queues << Sidekiq::Fetcher::TIMEOUT
79
+ if @strictly_ordered_queues
80
+ @queues
81
+ else
82
+ permute = @queues.shuffle
83
+ permute.uniq!
84
+ permute
85
+ end
154
86
  end
155
87
  end
156
88
  end
@@ -0,0 +1,53 @@
1
+ require "sidekiq/job/iterable"
2
+
3
+ # Iterable jobs are ones which provide a sequence to process using
4
+ # `build_enumerator(*args, cursor: cursor)` and then process each
5
+ # element of that sequence in `each_iteration(item, *args)`.
6
+ #
7
+ # The job is kicked off as normal:
8
+ #
9
+ # ProcessUserSet.perform_async(123)
10
+ #
11
+ # but instead of calling `perform`, Sidekiq will call:
12
+ #
13
+ # enum = ProcessUserSet#build_enumerator(123, cursor:nil)
14
+ #
15
+ # Your Enumerator must yield `(object, updated_cursor)` and
16
+ # Sidekiq will call your `each_iteration` method:
17
+ #
18
+ # ProcessUserSet#each_iteration(object, 123)
19
+ #
20
+ # After every iteration, Sidekiq will check for shutdown. If we are
21
+ # stopping, the cursor will be saved to Redis and the job re-queued
22
+ # to pick up the rest of the work upon restart. Your job will get
23
+ # the updated_cursor so it can pick up right where it stopped.
24
+ #
25
+ # enum = ProcessUserSet#build_enumerator(123, cursor: updated_cursor)
26
+ #
27
+ # The cursor object must be serializable to JSON.
28
+ #
29
+ # Note there are several APIs to help you build enumerators for
30
+ # ActiveRecord Relations, CSV files, etc. See sidekiq/job/iterable/*.rb.
31
+ module Sidekiq
32
+ module IterableJob
33
+ def self.included(base)
34
+ base.include Sidekiq::Job
35
+ base.include Sidekiq::Job::Iterable
36
+ end
37
+
38
+ # def build_enumerator(*args, cursor:)
39
+ # def each_iteration(item, *args)
40
+
41
+ # Your job can also define several callbacks during points
42
+ # in each job's lifecycle.
43
+ #
44
+ # def on_start
45
+ # def on_resume
46
+ # def on_stop
47
+ # def on_complete
48
+ # def around_iteration
49
+ #
50
+ # To keep things simple and compatible, this is the same
51
+ # API as the `sidekiq-iteration` gem.
52
+ end
53
+ end
@@ -0,0 +1,22 @@
1
+ module Sidekiq
2
+ module Job
3
+ class InterruptHandler
4
+ include Sidekiq::ServerMiddleware
5
+
6
+ def call(instance, hash, queue)
7
+ yield
8
+ rescue Interrupted
9
+ logger.debug "Interrupted, re-queueing..."
10
+ c = Sidekiq::Client.new
11
+ c.push(hash)
12
+ raise Sidekiq::JobRetry::Skip
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Sidekiq.configure_server do |config|
19
+ config.server_middleware do |chain|
20
+ chain.add Sidekiq::Job::InterruptHandler
21
+ end
22
+ end