sidekiq 6.2.2 → 8.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +726 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +70 -39
  5. data/bin/kiq +17 -0
  6. data/bin/lint-herb +13 -0
  7. data/bin/multi_queue_bench +271 -0
  8. data/bin/sidekiq +4 -9
  9. data/bin/sidekiqload +214 -115
  10. data/bin/sidekiqmon +4 -1
  11. data/bin/webload +69 -0
  12. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  13. data/lib/generators/sidekiq/job_generator.rb +71 -0
  14. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
  15. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  16. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  17. data/lib/sidekiq/api.rb +729 -264
  18. data/lib/sidekiq/capsule.rb +135 -0
  19. data/lib/sidekiq/cli.rb +124 -100
  20. data/lib/sidekiq/client.rb +153 -106
  21. data/lib/sidekiq/component.rb +132 -0
  22. data/lib/sidekiq/config.rb +320 -0
  23. data/lib/sidekiq/deploy.rb +64 -0
  24. data/lib/sidekiq/embedded.rb +64 -0
  25. data/lib/sidekiq/fetch.rb +27 -26
  26. data/lib/sidekiq/iterable_job.rb +56 -0
  27. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  28. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  29. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  30. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  31. data/lib/sidekiq/job/iterable.rb +322 -0
  32. data/lib/sidekiq/job.rb +397 -5
  33. data/lib/sidekiq/job_logger.rb +23 -32
  34. data/lib/sidekiq/job_retry.rb +141 -68
  35. data/lib/sidekiq/job_util.rb +113 -0
  36. data/lib/sidekiq/launcher.rb +122 -98
  37. data/lib/sidekiq/loader.rb +57 -0
  38. data/lib/sidekiq/logger.rb +27 -106
  39. data/lib/sidekiq/manager.rb +41 -43
  40. data/lib/sidekiq/metrics/query.rb +184 -0
  41. data/lib/sidekiq/metrics/shared.rb +109 -0
  42. data/lib/sidekiq/metrics/tracking.rb +153 -0
  43. data/lib/sidekiq/middleware/chain.rb +96 -51
  44. data/lib/sidekiq/middleware/current_attributes.rb +120 -0
  45. data/lib/sidekiq/middleware/i18n.rb +8 -4
  46. data/lib/sidekiq/middleware/modules.rb +23 -0
  47. data/lib/sidekiq/monitor.rb +16 -6
  48. data/lib/sidekiq/paginator.rb +37 -10
  49. data/lib/sidekiq/processor.rb +105 -87
  50. data/lib/sidekiq/profiler.rb +73 -0
  51. data/lib/sidekiq/rails.rb +49 -36
  52. data/lib/sidekiq/redis_client_adapter.rb +117 -0
  53. data/lib/sidekiq/redis_connection.rb +55 -86
  54. data/lib/sidekiq/ring_buffer.rb +32 -0
  55. data/lib/sidekiq/scheduled.rb +106 -50
  56. data/lib/sidekiq/systemd.rb +2 -0
  57. data/lib/sidekiq/test_api.rb +331 -0
  58. data/lib/sidekiq/testing/inline.rb +2 -30
  59. data/lib/sidekiq/testing.rb +2 -342
  60. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  61. data/lib/sidekiq/tui/controls.rb +53 -0
  62. data/lib/sidekiq/tui/filtering.rb +53 -0
  63. data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  64. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  65. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  66. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  67. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  68. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  69. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  70. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  71. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  72. data/lib/sidekiq/tui/tabs.rb +15 -0
  73. data/lib/sidekiq/tui.rb +382 -0
  74. data/lib/sidekiq/version.rb +6 -1
  75. data/lib/sidekiq/web/action.rb +149 -64
  76. data/lib/sidekiq/web/application.rb +376 -268
  77. data/lib/sidekiq/web/config.rb +117 -0
  78. data/lib/sidekiq/web/helpers.rb +213 -87
  79. data/lib/sidekiq/web/router.rb +61 -74
  80. data/lib/sidekiq/web.rb +71 -100
  81. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  82. data/lib/sidekiq.rb +95 -196
  83. data/sidekiq.gemspec +14 -11
  84. data/web/assets/images/logo.png +0 -0
  85. data/web/assets/images/status.png +0 -0
  86. data/web/assets/javascripts/application.js +171 -57
  87. data/web/assets/javascripts/base-charts.js +120 -0
  88. data/web/assets/javascripts/chart.min.js +13 -0
  89. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  90. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  91. data/web/assets/javascripts/dashboard-charts.js +194 -0
  92. data/web/assets/javascripts/dashboard.js +41 -274
  93. data/web/assets/javascripts/metrics.js +280 -0
  94. data/web/assets/stylesheets/style.css +776 -0
  95. data/web/locales/ar.yml +72 -70
  96. data/web/locales/cs.yml +64 -62
  97. data/web/locales/da.yml +62 -53
  98. data/web/locales/de.yml +67 -65
  99. data/web/locales/el.yml +45 -24
  100. data/web/locales/en.yml +93 -69
  101. data/web/locales/es.yml +91 -68
  102. data/web/locales/fa.yml +67 -65
  103. data/web/locales/fr.yml +82 -67
  104. data/web/locales/gd.yml +110 -0
  105. data/web/locales/he.yml +67 -64
  106. data/web/locales/hi.yml +61 -59
  107. data/web/locales/it.yml +94 -54
  108. data/web/locales/ja.yml +74 -68
  109. data/web/locales/ko.yml +54 -52
  110. data/web/locales/lt.yml +68 -66
  111. data/web/locales/nb.yml +63 -61
  112. data/web/locales/nl.yml +54 -52
  113. data/web/locales/pl.yml +47 -45
  114. data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
  115. data/web/locales/pt.yml +53 -51
  116. data/web/locales/ru.yml +69 -66
  117. data/web/locales/sv.yml +55 -53
  118. data/web/locales/ta.yml +62 -60
  119. data/web/locales/tr.yml +102 -0
  120. data/web/locales/uk.yml +87 -61
  121. data/web/locales/ur.yml +66 -64
  122. data/web/locales/vi.yml +69 -67
  123. data/web/locales/zh-CN.yml +107 -0
  124. data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
  125. data/web/views/_footer.html.erb +32 -0
  126. data/web/views/_job_info.html.erb +115 -0
  127. data/web/views/_metrics_period_select.html.erb +15 -0
  128. data/web/views/_nav.html.erb +45 -0
  129. data/web/views/_paging.html.erb +26 -0
  130. data/web/views/_poll_link.html.erb +4 -0
  131. data/web/views/_summary.html.erb +40 -0
  132. data/web/views/busy.html.erb +151 -0
  133. data/web/views/dashboard.html.erb +104 -0
  134. data/web/views/dead.html.erb +38 -0
  135. data/web/views/filtering.html.erb +6 -0
  136. data/web/views/layout.html.erb +26 -0
  137. data/web/views/metrics.html.erb +85 -0
  138. data/web/views/metrics_for_job.html.erb +58 -0
  139. data/web/views/morgue.html.erb +69 -0
  140. data/web/views/profiles.html.erb +43 -0
  141. data/web/views/queue.html.erb +57 -0
  142. data/web/views/queues.html.erb +46 -0
  143. data/web/views/retries.html.erb +77 -0
  144. data/web/views/retry.html.erb +39 -0
  145. data/web/views/scheduled.html.erb +64 -0
  146. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  147. metadata +130 -61
  148. data/LICENSE +0 -9
  149. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  150. data/lib/sidekiq/delay.rb +0 -41
  151. data/lib/sidekiq/exception_handler.rb +0 -27
  152. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  153. data/lib/sidekiq/extensions/active_record.rb +0 -43
  154. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  155. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  156. data/lib/sidekiq/util.rb +0 -95
  157. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  158. data/lib/sidekiq/worker.rb +0 -244
  159. data/web/assets/stylesheets/application-dark.css +0 -147
  160. data/web/assets/stylesheets/application-rtl.css +0 -246
  161. data/web/assets/stylesheets/application.css +0 -1053
  162. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  163. data/web/assets/stylesheets/bootstrap.css +0 -5
  164. data/web/locales/zh-cn.yml +0 -68
  165. data/web/views/_footer.erb +0 -20
  166. data/web/views/_job_info.erb +0 -89
  167. data/web/views/_nav.erb +0 -52
  168. data/web/views/_paging.erb +0 -23
  169. data/web/views/_poll_link.erb +0 -7
  170. data/web/views/_status.erb +0 -4
  171. data/web/views/_summary.erb +0 -40
  172. data/web/views/busy.erb +0 -132
  173. data/web/views/dashboard.erb +0 -83
  174. data/web/views/dead.erb +0 -34
  175. data/web/views/layout.erb +0 -42
  176. data/web/views/morgue.erb +0 -78
  177. data/web/views/queue.erb +0 -55
  178. data/web/views/queues.erb +0 -38
  179. data/web/views/retries.erb +0 -83
  180. data/web/views/retry.erb +0 -34
  181. data/web/views/scheduled.erb +0 -57
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
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
+ # if the Iterable job runs longer than this value (in seconds), then the job
21
+ # will be interrupted after the current iteration and re-enqueued at the back of the queue
22
+ max_iteration_runtime: nil,
23
+ error_handlers: [],
24
+ death_handlers: [],
25
+ lifecycle_events: {
26
+ startup: [],
27
+ quiet: [],
28
+ shutdown: [],
29
+ exit: [],
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
+ logged_job_attributes: ["bid", "tags"],
40
+ redis_idle_timeout: nil
41
+ }
42
+
43
+ ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
44
+ Sidekiq::Context.with(ctx) do
45
+ dev = cfg[:environment] == "development"
46
+ fancy = dev && $stdout.tty? # 🎩
47
+ # Weird logic here but we want to show the backtrace in local
48
+ # development or if verbose logging is enabled.
49
+ #
50
+ # `full_message` contains the error class, message and backtrace
51
+ # `detailed_message` contains the error class and message
52
+ #
53
+ # Absolutely terrible API names. Not useful at all to have two
54
+ # methods with similar but obscure names.
55
+ if dev || cfg.logger.debug?
56
+ cfg.logger.info { ex.full_message(highlight: fancy) }
57
+ else
58
+ cfg.logger.info { ex.detailed_message(highlight: fancy) }
59
+ end
60
+ end
61
+ }
62
+
63
+ def initialize(options = {})
64
+ @options = DEFAULTS.merge(options)
65
+ @options[:error_handlers] << ERROR_HANDLER if @options[:error_handlers].empty?
66
+ @directory = {}
67
+ @redis_config = {}
68
+ @capsules = {}
69
+ end
70
+
71
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
72
+ attr_reader :capsules
73
+ attr_accessor :thread_priority
74
+
75
+ def inspect
76
+ "#<#{self.class.name} @options=#{
77
+ @options.except(:lifecycle_events, :reloader, :death_handlers, :error_handlers).inspect
78
+ }>"
79
+ end
80
+
81
+ def to_json(*)
82
+ Sidekiq.dump_json(@options)
83
+ end
84
+
85
+ # LEGACY: edits the default capsule
86
+ # config.concurrency = 5
87
+ def concurrency=(val)
88
+ default_capsule.concurrency = Integer(val)
89
+ end
90
+
91
+ def concurrency
92
+ default_capsule.concurrency
93
+ end
94
+
95
+ def total_concurrency
96
+ capsules.each_value.sum(&:concurrency)
97
+ end
98
+
99
+ # Edit the default capsule.
100
+ # config.queues = %w( high default low ) # strict
101
+ # config.queues = %w( high,3 default,2 low,1 ) # weighted
102
+ # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
103
+ #
104
+ # With weighted priority, queue will be checked first (weight / total) of the time.
105
+ # high will be checked first (3/6) or 50% of the time.
106
+ # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
107
+ # are ridiculous and unnecessarily expensive. You can get random queue ordering
108
+ # by explicitly setting all weights to 1.
109
+ def queues=(val)
110
+ default_capsule.queues = val
111
+ end
112
+
113
+ def queues
114
+ default_capsule.queues
115
+ end
116
+
117
+ def client_middleware
118
+ @client_chain ||= Sidekiq::Middleware::Chain.new(self)
119
+ yield @client_chain if block_given?
120
+ @client_chain
121
+ end
122
+
123
+ def server_middleware
124
+ @server_chain ||= Sidekiq::Middleware::Chain.new(self)
125
+ yield @server_chain if block_given?
126
+ @server_chain
127
+ end
128
+
129
+ def default_capsule(&block)
130
+ capsule("default", &block)
131
+ end
132
+
133
+ # register a new queue processing subsystem
134
+ def capsule(name)
135
+ nm = name.to_s
136
+ cap = @capsules.fetch(nm) do
137
+ cap = Sidekiq::Capsule.new(nm, self)
138
+ @capsules[nm] = cap
139
+ end
140
+ yield cap if block_given?
141
+ cap
142
+ end
143
+
144
+ # All capsules must use the same Redis configuration
145
+ def redis=(hash)
146
+ @redis_config = @redis_config.merge(hash)
147
+ end
148
+
149
+ def reap_idle_redis_connections(timeout = 60)
150
+ self[:redis_idle_timeout] = timeout
151
+ end
152
+
153
+ def redis_pool
154
+ Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
155
+ end
156
+
157
+ def local_redis_pool
158
+ # this is our internal client/housekeeping pool. each capsule has its
159
+ # own pool for executing threads.
160
+ @redis ||= new_redis_pool(10, "internal")
161
+ end
162
+
163
+ def new_redis_pool(size, name = "unset")
164
+ # connection pool is lazy, it will not create connections unless you actually need them
165
+ # so don't be skimpy!
166
+ RedisConnection.create({size: size, logger: logger, pool_name: name}.merge(@redis_config))
167
+ end
168
+
169
+ def redis_info
170
+ redis do |conn|
171
+ conn.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
172
+ rescue RedisClientAdapter::CommandError => ex
173
+ # 2850 return fake version when INFO command has (probably) been renamed
174
+ raise unless /unknown command/.match?(ex.message)
175
+ {
176
+ "redis_version" => "9.9.9",
177
+ "uptime_in_days" => "9999",
178
+ "connected_clients" => "9999",
179
+ "used_memory_human" => "9P",
180
+ "used_memory_peak_human" => "9P"
181
+ }.freeze
182
+ end
183
+ end
184
+
185
+ def redis
186
+ raise ArgumentError, "requires a block" unless block_given?
187
+ redis_pool.with do |conn|
188
+ retryable = true
189
+ begin
190
+ yield conn
191
+ rescue RedisClientAdapter::BaseError => ex
192
+ # 2550 Failover can cause the server to become a replica, need
193
+ # to disconnect and reopen the socket to get back to the primary.
194
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
195
+ # 4985 Use the same logic when a blocking command is force-unblocked
196
+ # The same retry logic is also used in client.rb
197
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
198
+ conn.close
199
+ retryable = false
200
+ retry
201
+ end
202
+ raise
203
+ end
204
+ end
205
+ end
206
+
207
+ # register global singletons which can be accessed elsewhere
208
+ def register(name, instance)
209
+ # logger.debug("register[#{name}] = #{instance}")
210
+ # Sidekiq Enterprise lazy registers a few services so we
211
+ # can't lock down this hash completely.
212
+ hash = @directory.dup
213
+ hash[name] = instance
214
+ @directory = hash.freeze
215
+ instance
216
+ end
217
+
218
+ # find a singleton
219
+ def lookup(name, default_class = nil)
220
+ # JNDI is just a fancy name for a hash lookup
221
+ @directory.fetch(name) do |key|
222
+ return nil unless default_class
223
+ register(key, default_class.new(self))
224
+ end
225
+ end
226
+
227
+ def freeze!
228
+ @directory.freeze
229
+ @options.freeze
230
+ true
231
+ end
232
+
233
+ ##
234
+ # Death handlers are called when all retries for a job have been exhausted and
235
+ # the job dies. It's the notification to your application
236
+ # that this job will not succeed without manual intervention.
237
+ #
238
+ # Sidekiq.configure_server do |config|
239
+ # config.death_handlers << ->(job, ex) do
240
+ # end
241
+ # end
242
+ def death_handlers
243
+ @options[:death_handlers]
244
+ end
245
+
246
+ # How frequently Redis should be checked by a random Sidekiq process for
247
+ # scheduled and retriable jobs. Each individual process will take turns by
248
+ # waiting some multiple of this value.
249
+ #
250
+ # See sidekiq/scheduled.rb for an in-depth explanation of this value
251
+ def average_scheduled_poll_interval=(interval)
252
+ @options[:average_scheduled_poll_interval] = interval
253
+ end
254
+
255
+ # Register a proc to handle any error which occurs within the Sidekiq process.
256
+ #
257
+ # Sidekiq.configure_server do |config|
258
+ # config.error_handlers << proc {|ex,ctx_hash,config| MyErrorService.notify(ex, ctx_hash) }
259
+ # end
260
+ #
261
+ # The default error handler logs errors to @logger.
262
+ def error_handlers
263
+ @options[:error_handlers]
264
+ end
265
+
266
+ # Register a block to run at a point in the Sidekiq lifecycle.
267
+ # :startup, :quiet, :shutdown, or :exit are valid events.
268
+ #
269
+ # Sidekiq.configure_server do |config|
270
+ # config.on(:shutdown) do
271
+ # puts "Goodbye cruel world!"
272
+ # end
273
+ # end
274
+ def on(event, &block)
275
+ raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
276
+ raise ArgumentError, "Invalid event name: #{event}" unless @options[:lifecycle_events].key?(event)
277
+ @options[:lifecycle_events][event] << block
278
+ end
279
+
280
+ def logger
281
+ @logger ||= Sidekiq::Logger.new($stdout, level: :info).tap do |log|
282
+ log.level = Logger::INFO
283
+ log.formatter = if ENV["DYNO"]
284
+ Sidekiq::Logger::Formatters::WithoutTimestamp.new
285
+ else
286
+ Sidekiq::Logger::Formatters::Pretty.new
287
+ end
288
+ end
289
+ end
290
+
291
+ def logger=(logger)
292
+ if logger.nil?
293
+ self.logger.level = Logger::FATAL
294
+ return
295
+ end
296
+
297
+ @logger = logger
298
+ end
299
+
300
+ private def parameter_size(handler)
301
+ target = handler.is_a?(Proc) ? handler : handler.method(:call)
302
+ target.parameters.size
303
+ end
304
+
305
+ # INTERNAL USE ONLY
306
+ def handle_exception(ex, ctx = {})
307
+ if @options[:error_handlers].size == 0
308
+ p ["!!!!!", ex]
309
+ end
310
+ @options[:error_handlers].each do |handler|
311
+ handler.call(ex, ctx, self)
312
+ rescue Exception => e
313
+ l = logger
314
+ l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
315
+ l.error e
316
+ l.error e.backtrace.join("\n") unless e.backtrace.nil?
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/redis_connection"
4
+ require "time"
5
+
6
+ # This file is designed to be required within the user's
7
+ # deployment script; it should need a bare minimum of dependencies.
8
+ # Usage:
9
+ #
10
+ # require "sidekiq/deploy"
11
+ # Sidekiq::Deploy.mark!("Some change")
12
+ #
13
+ # If you do not pass a label, Sidekiq will try to use the latest
14
+ # git commit info.
15
+ #
16
+
17
+ module Sidekiq
18
+ class Deploy
19
+ MARK_TTL = 90 * 24 * 60 * 60 # 90 days
20
+
21
+ LABEL_MAKER = -> {
22
+ `git log -1 --format="%h %s"`.strip
23
+ }
24
+
25
+ def self.mark!(label = nil)
26
+ Sidekiq::Deploy.new.mark!(label: label)
27
+ end
28
+
29
+ def initialize(pool = Sidekiq::RedisConnection.create)
30
+ @pool = pool
31
+ end
32
+
33
+ def mark!(at: Time.now, label: nil)
34
+ label ||= LABEL_MAKER.call
35
+ # we need to round the timestamp so that we gracefully
36
+ # handle an very common error in marking deploys:
37
+ # having every process mark its deploy, leading
38
+ # to N marks for each deploy. Instead we round the time
39
+ # to the minute so that multiple marks within that minute
40
+ # will all naturally rollup into one mark per minute.
41
+ whence = at.utc
42
+ floor = Time.utc(whence.year, whence.month, whence.mday, whence.hour, whence.min, 0)
43
+ datecode = floor.strftime("%Y%m%d")
44
+ key = "#{datecode}-marks"
45
+ stamp = floor.iso8601
46
+
47
+ @pool.with do |c|
48
+ # only allow one deploy mark for a given label for the next minute
49
+ lock = c.set("deploylock-#{label}", stamp, "nx", "ex", "60")
50
+ if lock
51
+ c.multi do |pipe|
52
+ pipe.hsetnx(key, stamp, label)
53
+ pipe.expire(key, MARK_TTL)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def fetch(date = Time.now.utc.to_date)
60
+ datecode = date.strftime("%Y%m%d")
61
+ @pool.with { |c| c.hgetall("#{datecode}-marks") }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/component"
4
+ require "sidekiq/launcher"
5
+ require "sidekiq/metrics/tracking"
6
+
7
+ module Sidekiq
8
+ class Embedded
9
+ include Sidekiq::Component
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ def run
16
+ housekeeping
17
+ fire_event(:startup, reverse: false, reraise: true)
18
+ @launcher = Sidekiq::Launcher.new(@config, embedded: true)
19
+ @launcher.run
20
+ sleep 0.2 # pause to give threads time to spin up
21
+
22
+ logger.info "Sidekiq running embedded, total process thread count: #{Thread.list.size}"
23
+ logger.debug { Thread.list.map(&:name) }
24
+ end
25
+
26
+ def quiet
27
+ @launcher&.quiet
28
+ end
29
+
30
+ def stop
31
+ @launcher&.stop
32
+ end
33
+
34
+ private
35
+
36
+ def housekeeping
37
+ @config[:tag] ||= default_tag
38
+ logger.info "Running in #{RUBY_DESCRIPTION}"
39
+ logger.info Sidekiq::LICENSE
40
+ logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
41
+
42
+ # touch the connection pool so it is created before we
43
+ # fire startup and start multithreading.
44
+ info = config.redis_info
45
+ ver = Gem::Version.new(info["redis_version"])
46
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
47
+
48
+ maxmemory_policy = info["maxmemory_policy"]
49
+ if maxmemory_policy != "noeviction"
50
+ logger.warn <<~EOM
51
+
52
+
53
+ WARNING: Your Redis instance will evict Sidekiq data under heavy load.
54
+ The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
55
+ See: https://github.com/sidekiq/sidekiq/wiki/Using-Redis#memory
56
+
57
+ EOM
58
+ end
59
+
60
+ logger.debug { "Client Middleware: #{@config.default_capsule.client_middleware.map(&:klass).join(", ")}" }
61
+ logger.debug { "Server Middleware: #{@config.default_capsule.server_middleware.map(&:klass).join(", ")}" }
62
+ end
63
+ end
64
+ end
data/lib/sidekiq/fetch.rb CHANGED
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq"
4
+ require "sidekiq/component"
5
+ require "sidekiq/capsule"
4
6
 
5
- module Sidekiq
7
+ module Sidekiq # :nodoc:
6
8
  class BasicFetch
9
+ include Sidekiq::Component
10
+
7
11
  # We want the fetch operation to timeout every few seconds so the thread
8
12
  # can check if the process is shutting down.
9
13
  TIMEOUT = 2
10
14
 
11
- UnitOfWork = Struct.new(:queue, :job) {
15
+ UnitOfWork = Struct.new(:queue, :job, :config) {
12
16
  def acknowledge
13
17
  # nothing to do
14
18
  end
@@ -18,56 +22,53 @@ module Sidekiq
18
22
  end
19
23
 
20
24
  def requeue
21
- Sidekiq.redis do |conn|
25
+ config.redis do |conn|
22
26
  conn.rpush(queue, job)
23
27
  end
24
28
  end
25
29
  }
26
30
 
27
- def initialize(options)
28
- raise ArgumentError, "missing queue list" unless options[:queues]
29
- @options = options
30
- @strictly_ordered_queues = !!@options[:strict]
31
- @queues = @options[:queues].map { |q| "queue:#{q}" }
32
- if @strictly_ordered_queues
33
- @queues.uniq!
34
- @queues << TIMEOUT
35
- end
31
+ def initialize(cap)
32
+ raise ArgumentError, "missing queue list" unless cap.queues
33
+ @config = cap
34
+ @strictly_ordered_queues = cap.mode == :strict
35
+ @queues = config.queues.map { |q| "queue:#{q}" }
36
+ @queues.uniq! if @strictly_ordered_queues
36
37
  end
37
38
 
38
39
  def retrieve_work
39
40
  qs = queues_cmd
40
41
  # 4825 Sidekiq Pro with all queues paused will return an
41
- # empty set of queues with a trailing TIMEOUT value.
42
- if qs.size <= 1
42
+ # empty set of queues
43
+ if qs.size <= 0
43
44
  sleep(TIMEOUT)
44
45
  return nil
45
46
  end
46
47
 
47
- work = Sidekiq.redis { |conn| conn.brpop(*qs) }
48
- UnitOfWork.new(*work) if work
48
+ queue, job = redis { |conn| conn.blocking_call(TIMEOUT, "brpop", *qs, TIMEOUT) }
49
+ UnitOfWork.new(queue, job, config) if queue
49
50
  end
50
51
 
51
- def bulk_requeue(inprogress, options)
52
+ def bulk_requeue(inprogress)
52
53
  return if inprogress.empty?
53
54
 
54
- Sidekiq.logger.debug { "Re-queueing terminated jobs" }
55
+ logger.debug { "Re-queueing terminated jobs" }
55
56
  jobs_to_requeue = {}
56
57
  inprogress.each do |unit_of_work|
57
58
  jobs_to_requeue[unit_of_work.queue] ||= []
58
59
  jobs_to_requeue[unit_of_work.queue] << unit_of_work.job
59
60
  end
60
61
 
61
- Sidekiq.redis do |conn|
62
- conn.pipelined do
62
+ redis do |conn|
63
+ conn.pipelined do |pipeline|
63
64
  jobs_to_requeue.each do |queue, jobs|
64
- conn.rpush(queue, jobs)
65
+ pipeline.rpush(queue, jobs)
65
66
  end
66
67
  end
67
68
  end
68
- Sidekiq.logger.info("Pushed #{inprogress.size} jobs back to Redis")
69
+ logger.info("Pushed #{inprogress.size} jobs back to Redis")
69
70
  rescue => ex
70
- Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
71
+ logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
71
72
  end
72
73
 
73
74
  # Creating the Redis#brpop command takes into account any
@@ -79,9 +80,9 @@ module Sidekiq
79
80
  if @strictly_ordered_queues
80
81
  @queues
81
82
  else
82
- queues = @queues.shuffle!.uniq
83
- queues << TIMEOUT
84
- queues
83
+ permute = @queues.shuffle
84
+ permute.uniq!
85
+ permute
85
86
  end
86
87
  end
87
88
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/job/iterable"
4
+
5
+ # Iterable jobs are ones which provide a sequence to process using
6
+ # `build_enumerator(*args, cursor: cursor)` and then process each
7
+ # element of that sequence in `each_iteration(item, *args)`.
8
+ #
9
+ # The job is kicked off as normal:
10
+ #
11
+ # ProcessUserSet.perform_async(123)
12
+ #
13
+ # but instead of calling `perform`, Sidekiq will call:
14
+ #
15
+ # enum = ProcessUserSet#build_enumerator(123, cursor:nil)
16
+ #
17
+ # Your Enumerator must yield `(object, updated_cursor)` and
18
+ # Sidekiq will call your `each_iteration` method:
19
+ #
20
+ # ProcessUserSet#each_iteration(object, 123)
21
+ #
22
+ # After every iteration, Sidekiq will check for shutdown. If we are
23
+ # stopping, the cursor will be saved to Redis and the job re-queued
24
+ # to pick up the rest of the work upon restart. Your job will get
25
+ # the updated_cursor so it can pick up right where it stopped.
26
+ #
27
+ # enum = ProcessUserSet#build_enumerator(123, cursor: updated_cursor)
28
+ #
29
+ # The cursor object must be serializable to JSON.
30
+ #
31
+ # Note there are several APIs to help you build enumerators for
32
+ # ActiveRecord Relations, CSV files, etc. See sidekiq/job/iterable/*.rb.
33
+ module Sidekiq
34
+ module IterableJob
35
+ def self.included(base)
36
+ base.include Sidekiq::Job
37
+ base.include Sidekiq::Job::Iterable
38
+ end
39
+
40
+ # def build_enumerator(*args, cursor:)
41
+ # def each_iteration(item, *args)
42
+
43
+ # Your job can also define several callbacks during points
44
+ # in each job's lifecycle.
45
+ #
46
+ # def on_start
47
+ # def on_resume
48
+ # def on_stop
49
+ # def on_cancel
50
+ # def on_complete
51
+ # def around_iteration
52
+ #
53
+ # To keep things simple and compatible, this is the same
54
+ # API as the `sidekiq-iteration` gem.
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ class InterruptHandler
6
+ include Sidekiq::ServerMiddleware
7
+
8
+ def call(instance, hash, queue)
9
+ yield
10
+ rescue Interrupted
11
+ logger.debug "Interrupted, re-queueing..."
12
+ c = Sidekiq::Client.new
13
+ c.push(hash)
14
+ raise Sidekiq::JobRetry::Skip
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Sidekiq.configure_server do |config|
21
+ config.server_middleware do |chain|
22
+ chain.add Sidekiq::Job::InterruptHandler
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ module Iterable
6
+ # @api private
7
+ class ActiveRecordEnumerator
8
+ def initialize(relation, cursor: nil, **options)
9
+ @relation = relation
10
+ @cursor = cursor
11
+ @options = options
12
+ end
13
+
14
+ def records
15
+ Enumerator.new(-> { @relation.count }) do |yielder|
16
+ @relation.find_each(**@options, start: @cursor) do |record|
17
+ yielder.yield(record, record.id)
18
+ end
19
+ end
20
+ end
21
+
22
+ def batches
23
+ Enumerator.new(-> { @relation.count }) do |yielder|
24
+ @relation.find_in_batches(**@options, start: @cursor) do |batch|
25
+ yielder.yield(batch, batch.first.id)
26
+ end
27
+ end
28
+ end
29
+
30
+ def relations
31
+ Enumerator.new(-> { relations_size }) do |yielder|
32
+ # Convenience to use :batch_size for all the
33
+ # ActiveRecord batching methods.
34
+ options = @options.dup
35
+ options[:of] ||= options.delete(:batch_size)
36
+
37
+ @relation.in_batches(**options, start: @cursor) do |relation|
38
+ first_record = relation.first
39
+ yielder.yield(relation, first_record.id)
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def relations_size
47
+ batch_size = @options[:batch_size] || 1000
48
+ (@relation.count + batch_size - 1) / batch_size # ceiling division
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end