sidekiq 7.3.9 → 8.0.0.beta2

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +28 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +10 -10
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +5 -5
  7. data/lib/sidekiq/api.rb +120 -36
  8. data/lib/sidekiq/capsule.rb +6 -6
  9. data/lib/sidekiq/cli.rb +15 -19
  10. data/lib/sidekiq/client.rb +13 -16
  11. data/lib/sidekiq/component.rb +40 -2
  12. data/lib/sidekiq/config.rb +18 -15
  13. data/lib/sidekiq/embedded.rb +1 -0
  14. data/lib/sidekiq/iterable_job.rb +1 -0
  15. data/lib/sidekiq/job/iterable.rb +13 -4
  16. data/lib/sidekiq/job_retry.rb +17 -5
  17. data/lib/sidekiq/job_util.rb +5 -1
  18. data/lib/sidekiq/launcher.rb +1 -1
  19. data/lib/sidekiq/logger.rb +6 -10
  20. data/lib/sidekiq/manager.rb +0 -1
  21. data/lib/sidekiq/metrics/query.rb +71 -45
  22. data/lib/sidekiq/metrics/shared.rb +4 -1
  23. data/lib/sidekiq/metrics/tracking.rb +9 -7
  24. data/lib/sidekiq/middleware/current_attributes.rb +5 -17
  25. data/lib/sidekiq/paginator.rb +8 -1
  26. data/lib/sidekiq/processor.rb +21 -14
  27. data/lib/sidekiq/profiler.rb +59 -0
  28. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  29. data/lib/sidekiq/testing.rb +2 -2
  30. data/lib/sidekiq/version.rb +2 -2
  31. data/lib/sidekiq/web/action.rb +104 -84
  32. data/lib/sidekiq/web/application.rb +347 -332
  33. data/lib/sidekiq/web/config.rb +116 -0
  34. data/lib/sidekiq/web/helpers.rb +41 -16
  35. data/lib/sidekiq/web/router.rb +60 -76
  36. data/lib/sidekiq/web.rb +51 -156
  37. data/lib/sidekiq.rb +1 -1
  38. data/sidekiq.gemspec +5 -4
  39. data/web/assets/javascripts/application.js +6 -13
  40. data/web/assets/javascripts/base-charts.js +30 -16
  41. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  42. data/web/assets/javascripts/metrics.js +16 -34
  43. data/web/assets/stylesheets/style.css +750 -0
  44. data/web/locales/ar.yml +1 -0
  45. data/web/locales/cs.yml +1 -0
  46. data/web/locales/da.yml +1 -0
  47. data/web/locales/de.yml +1 -0
  48. data/web/locales/el.yml +1 -0
  49. data/web/locales/en.yml +6 -0
  50. data/web/locales/es.yml +24 -2
  51. data/web/locales/fa.yml +1 -0
  52. data/web/locales/fr.yml +1 -0
  53. data/web/locales/gd.yml +1 -0
  54. data/web/locales/he.yml +1 -0
  55. data/web/locales/hi.yml +1 -0
  56. data/web/locales/it.yml +1 -0
  57. data/web/locales/ja.yml +1 -0
  58. data/web/locales/ko.yml +1 -0
  59. data/web/locales/lt.yml +1 -0
  60. data/web/locales/nb.yml +1 -0
  61. data/web/locales/nl.yml +1 -0
  62. data/web/locales/pl.yml +1 -0
  63. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  64. data/web/locales/pt.yml +1 -0
  65. data/web/locales/ru.yml +1 -0
  66. data/web/locales/sv.yml +1 -0
  67. data/web/locales/ta.yml +1 -0
  68. data/web/locales/tr.yml +1 -0
  69. data/web/locales/uk.yml +1 -0
  70. data/web/locales/ur.yml +1 -0
  71. data/web/locales/vi.yml +1 -0
  72. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  73. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  74. data/web/views/_footer.erb +31 -33
  75. data/web/views/_job_info.erb +91 -89
  76. data/web/views/_metrics_period_select.erb +13 -10
  77. data/web/views/_nav.erb +14 -21
  78. data/web/views/_paging.erb +23 -21
  79. data/web/views/_poll_link.erb +2 -2
  80. data/web/views/_summary.erb +16 -16
  81. data/web/views/busy.erb +124 -122
  82. data/web/views/dashboard.erb +62 -66
  83. data/web/views/dead.erb +31 -27
  84. data/web/views/filtering.erb +3 -3
  85. data/web/views/layout.erb +6 -22
  86. data/web/views/metrics.erb +75 -81
  87. data/web/views/metrics_for_job.erb +45 -46
  88. data/web/views/morgue.erb +61 -70
  89. data/web/views/profiles.erb +43 -0
  90. data/web/views/queue.erb +54 -52
  91. data/web/views/queues.erb +43 -41
  92. data/web/views/retries.erb +66 -75
  93. data/web/views/retry.erb +32 -27
  94. data/web/views/scheduled.erb +58 -54
  95. data/web/views/scheduled_job_info.erb +1 -1
  96. metadata +32 -18
  97. data/web/assets/stylesheets/application-dark.css +0 -147
  98. data/web/assets/stylesheets/application-rtl.css +0 -163
  99. data/web/assets/stylesheets/application.css +0 -759
  100. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  101. data/web/assets/stylesheets/bootstrap.css +0 -5
  102. data/web/views/_status.erb +0 -4
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_job/arguments"
3
4
  require "active_support/current_attributes"
4
5
 
5
6
  module Sidekiq
@@ -20,6 +21,8 @@ module Sidekiq
20
21
  # Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
21
22
  #
22
23
  module CurrentAttributes
24
+ Serializer = ::ActiveJob::Arguments
25
+
23
26
  class Save
24
27
  include Sidekiq::ClientMiddleware
25
28
 
@@ -33,26 +36,11 @@ module Sidekiq
33
36
  attrs = strklass.constantize.attributes
34
37
  # Retries can push the job N times, we don't
35
38
  # want retries to reset cattr. #5692, #5090
36
- if attrs.any?
37
- # Older rails has a bug that `CurrentAttributes#attributes` always returns
38
- # the same hash instance. We need to dup it to avoid being accidentally mutated.
39
- job[key] = if returns_same_object?
40
- attrs.dup
41
- else
42
- attrs
43
- end
44
- end
39
+ job[key] = Serializer.serialize(attrs) if attrs.any?
45
40
  end
46
41
  end
47
42
  yield
48
43
  end
49
-
50
- private
51
-
52
- def returns_same_object?
53
- ActiveSupport::VERSION::MAJOR < 8 ||
54
- (ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
55
- end
56
44
  end
57
45
 
58
46
  class Load
@@ -68,7 +56,7 @@ module Sidekiq
68
56
  @cattrs.each do |(key, strklass)|
69
57
  next unless job.has_key?(key)
70
58
 
71
- klass_attrs[strklass.constantize] = job[key]
59
+ klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
72
60
  end
73
61
 
74
62
  wrap(klass_attrs.to_a, &block)
@@ -17,7 +17,14 @@ module Sidekiq
17
17
  ending = starting + page_size - 1
18
18
 
19
19
  Sidekiq.redis do |conn|
20
- type = conn.type(key)
20
+ # horrible, think you can make this cleaner?
21
+ type = TYPE_CACHE[key]
22
+ if type
23
+ elsif key.start_with?("queue:")
24
+ type = TYPE_CACHE[key] = "list"
25
+ else
26
+ type = TYPE_CACHE[key] = conn.type(key)
27
+ end
21
28
  rev = opts && opts[:reverse]
22
29
 
23
30
  case type
@@ -3,6 +3,7 @@
3
3
  require "sidekiq/fetch"
4
4
  require "sidekiq/job_logger"
5
5
  require "sidekiq/job_retry"
6
+ require "sidekiq/profiler"
6
7
 
7
8
  module Sidekiq
8
9
  ##
@@ -66,7 +67,7 @@ module Sidekiq
66
67
  @thread ||= safe_thread("#{config.name}/processor", &method(:run))
67
68
  end
68
69
 
69
- private unless $TESTING
70
+ private
70
71
 
71
72
  def run
72
73
  # By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
@@ -112,13 +113,17 @@ module Sidekiq
112
113
  def handle_fetch_exception(ex)
113
114
  unless @down
114
115
  @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
115
- logger.error("Error fetching job: #{ex}")
116
116
  handle_exception(ex)
117
117
  end
118
118
  sleep(1)
119
119
  nil
120
120
  end
121
121
 
122
+ def profile(job, &block)
123
+ return yield unless job["profile"]
124
+ Sidekiq::Profiler.new(config).call(job, &block)
125
+ end
126
+
122
127
  def dispatch(job_hash, queue, jobstr)
123
128
  # since middleware can mutate the job hash
124
129
  # we need to clone it to report the original
@@ -132,17 +137,19 @@ module Sidekiq
132
137
  @retrier.global(jobstr, queue) do
133
138
  @job_logger.call(job_hash, queue) do
134
139
  stats(jobstr, queue) do
135
- # Rails 5 requires a Reloader to wrap code execution. In order to
136
- # constantize the worker and instantiate an instance, we have to call
137
- # the Reloader. It handles code loading, db connection management, etc.
138
- # Effectively this block denotes a "unit of work" to Rails.
139
- @reloader.call do
140
- klass = Object.const_get(job_hash["class"])
141
- instance = klass.new
142
- instance.jid = job_hash["jid"]
143
- instance._context = self
144
- @retrier.local(instance, jobstr, queue) do
145
- yield instance
140
+ profile(job_hash) do
141
+ # Rails 5 requires a Reloader to wrap code execution. In order to
142
+ # constantize the worker and instantiate an instance, we have to call
143
+ # the Reloader. It handles code loading, db connection management, etc.
144
+ # Effectively this block denotes a "unit of work" to Rails.
145
+ @reloader.call do
146
+ klass = Object.const_get(job_hash["class"])
147
+ instance = klass.new
148
+ instance.jid = job_hash["jid"]
149
+ instance._context = self
150
+ @retrier.local(instance, jobstr, queue) do
151
+ yield instance
152
+ end
146
153
  end
147
154
  end
148
155
  end
@@ -165,7 +172,6 @@ module Sidekiq
165
172
  begin
166
173
  job_hash = Sidekiq.load_json(jobstr)
167
174
  rescue => ex
168
- handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
169
175
  now = Time.now.to_f
170
176
  redis do |conn|
171
177
  conn.multi do |xa|
@@ -174,6 +180,7 @@ module Sidekiq
174
180
  xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
175
181
  end
176
182
  end
183
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
177
184
  return uow.acknowledge
178
185
  end
179
186
 
@@ -0,0 +1,59 @@
1
+ require "fileutils"
2
+ require "sidekiq/component"
3
+
4
+ module Sidekiq
5
+ # Allows the user to profile jobs running in production.
6
+ # See details in the Profiling wiki page.
7
+ class Profiler
8
+ EXPIRY = 86400 # 1 day
9
+ DEFAULT_OPTIONS = {
10
+ mode: :wall
11
+ }
12
+
13
+ include Sidekiq::Component
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ def call(job, &block)
19
+ return yield unless job["profile"]
20
+
21
+ token = job["profile"]
22
+ type = job["class"]
23
+ jid = job["jid"]
24
+ started_at = Time.now
25
+ options = DEFAULT_OPTIONS.merge((job["profiler_options"] || {}).transform_keys!(&:to_sym))
26
+
27
+ rundata = {
28
+ started_at: started_at.to_i,
29
+ token: token,
30
+ type: type,
31
+ jid: jid,
32
+ # .gz extension tells Vernier to compress the data
33
+ filename: "#{token}-#{type}-#{jid}-#{started_at.strftime("%Y%m%d-%H%M%S")}.json.gz"
34
+ }
35
+
36
+ require "vernier"
37
+ begin
38
+ a = Time.now
39
+ rc = Vernier.profile(**options.merge(out: rundata[:filename]), &block)
40
+ b = Time.now
41
+
42
+ # Failed jobs will raise an exception on previous line and skip this
43
+ # block. Only successful jobs will persist profile data to Redis.
44
+ key = "#{token}-#{jid}"
45
+ data = File.read(rundata[:filename])
46
+ redis do |conn|
47
+ conn.multi do |m|
48
+ m.zadd("profiles", Time.now.to_f + EXPIRY, key)
49
+ m.hset(key, rundata.merge(elapsed: (b - a), data: data, size: data.bytesize))
50
+ m.expire(key, EXPIRY)
51
+ end
52
+ end
53
+ rc
54
+ ensure
55
+ FileUtils.rm_f(rundata[:filename])
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "redis_client"
5
4
  require "redis_client/decorator"
6
5
 
@@ -87,7 +87,7 @@ module Sidekiq
87
87
  if Sidekiq::Testing.fake?
88
88
  payloads.each do |job|
89
89
  job = Sidekiq.load_json(Sidekiq.dump_json(job))
90
- job["enqueued_at"] = Time.now.to_f unless job["at"]
90
+ job["enqueued_at"] = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) unless job["at"]
91
91
  Queues.push(job["queue"], job["class"], job)
92
92
  end
93
93
  true
@@ -329,6 +329,6 @@ module Sidekiq
329
329
  end
330
330
  end
331
331
 
332
- if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
332
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING # rubocop:disable Style/GlobalVars
333
333
  warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
334
334
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.3.9"
5
- MAJOR = 7
4
+ VERSION = "8.0.0.beta2"
5
+ MAJOR = 8
6
6
 
7
7
  def self.gem_version
8
8
  Gem::Version.new(VERSION)
@@ -1,115 +1,135 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Sidekiq
4
- class WebAction
5
- RACK_SESSION = "rack.session"
3
+ require "erb"
6
4
 
7
- attr_accessor :env, :block, :type
5
+ module Sidekiq
6
+ class Web
7
+ ##
8
+ # These instance methods are available to all executing ERB
9
+ # templates.
10
+ class Action
11
+ attr_accessor :env, :block
12
+
13
+ def initialize(env, block)
14
+ @_erb = false
15
+ @env = env
16
+ @block = block
17
+ end
8
18
 
9
- def settings
10
- Web.settings
11
- end
19
+ def config
20
+ env[:web_config]
21
+ end
12
22
 
13
- def request
14
- @request ||= ::Rack::Request.new(env)
15
- end
23
+ def request
24
+ @request ||= ::Rack::Request.new(env)
25
+ end
16
26
 
17
- def halt(res)
18
- throw :halt, [res, {Rack::CONTENT_TYPE => "text/plain"}, [res.to_s]]
19
- end
27
+ def halt(res)
28
+ throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
29
+ end
20
30
 
21
- def redirect(location)
22
- throw :halt, [302, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
23
- end
31
+ # external redirect
32
+ def redirect_to(url)
33
+ throw :halt, [302, {"Location" => url}, []]
34
+ end
24
35
 
25
- def reload_page
26
- current_location = request.referer.gsub(request.base_url, "")
27
- redirect current_location
28
- end
36
+ def header(key, value)
37
+ env["response_headers"][key] = value
38
+ end
29
39
 
30
- # deprecated, will warn in 8.0
31
- def params
32
- indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
40
+ # internal redirect
41
+ def redirect(location)
42
+ throw :halt, [302, {"Location" => "#{request.base_url}#{location}"}, []]
43
+ end
33
44
 
34
- indifferent_hash.merge! request.params
35
- route_params.each { |k, v| indifferent_hash[k.to_s] = v }
45
+ def reload_page
46
+ current_location = request.referer.gsub(request.base_url, "")
47
+ redirect current_location
48
+ end
36
49
 
37
- indifferent_hash
38
- end
50
+ # stuff after ? or form input
51
+ # uses String keys, no Symbols!
52
+ def url_params(key)
53
+ warn { "URL parameter `#{key}` should be accessed via String, not Symbol (at #{caller(3..3).first})" } if key.is_a?(Symbol)
54
+ request.params[key.to_s]
55
+ end
39
56
 
40
- # Use like `url_params("page")` within your action blocks
41
- def url_params(key)
42
- request.params[key]
43
- end
57
+ # variables embedded in path, `/metrics/:name`
58
+ # uses Symbol keys, no Strings!
59
+ def route_params(key)
60
+ warn { "Route parameter `#{key}` should be accessed via Symbol, not String (at #{caller(3..3).first})" } if key.is_a?(String)
61
+ env["rack.route_params"][key.to_sym]
62
+ end
44
63
 
45
- # Use like `route_params(:name)` within your action blocks
46
- # key is required in 8.0, nil is only used for backwards compatibility
47
- def route_params(key = nil)
48
- if key
49
- env[WebRouter::ROUTE_PARAMS][key]
50
- else
51
- env[WebRouter::ROUTE_PARAMS]
64
+ def params
65
+ warn { "Direct access to Rack parameters is discouraged, use `url_params` or `route_params` (at #{caller(3..3).first})" }
66
+ request.params
52
67
  end
53
- end
54
68
 
55
- def session
56
- env[RACK_SESSION]
57
- end
69
+ def session
70
+ env["rack.session"]
71
+ end
58
72
 
59
- def erb(content, options = {})
60
- if content.is_a? Symbol
61
- unless respond_to?(:"_erb_#{content}")
62
- views = options[:views] || Web.settings.views
63
- filename = "#{views}/#{content}.erb"
64
- src = ERB.new(File.read(filename)).src
65
-
66
- # Need to use lineno less by 1 because erb generates a
67
- # comment before the source code.
68
- WebAction.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
69
- def _erb_#{content}
70
- #{src}
71
- end
72
- RUBY
73
+ def erb(content, options = {})
74
+ if content.is_a? Symbol
75
+ unless respond_to?(:"_erb_#{content}")
76
+ views = options[:views] || Web.views
77
+ filename = "#{views}/#{content}.erb"
78
+ src = ERB.new(File.read(filename)).src
79
+
80
+ # Need to use lineno less by 1 because erb generates a
81
+ # comment before the source code.
82
+ Action.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
83
+ def _erb_#{content}
84
+ #{src}
85
+ end
86
+ RUBY
87
+ end
73
88
  end
74
- end
75
89
 
76
- if @_erb
77
- _erb(content, options[:locals])
78
- else
79
- @_erb = true
80
- content = _erb(content, options[:locals])
90
+ if @_erb
91
+ _erb(content, options[:locals])
92
+ else
93
+ @_erb = true
94
+ content = _erb(content, options[:locals])
81
95
 
82
- _render { content }
96
+ _render { content }
97
+ end
83
98
  end
84
- end
85
99
 
86
- def render(engine, content, options = {})
87
- raise "Only erb templates are supported" if engine != :erb
100
+ def render(engine, content, options = {})
101
+ raise "Only erb templates are supported" if engine != :erb
88
102
 
89
- erb(content, options)
90
- end
103
+ erb(content, options)
104
+ end
91
105
 
92
- def json(payload)
93
- [200, {Rack::CONTENT_TYPE => "application/json", Rack::CACHE_CONTROL => "private, no-store"}, [Sidekiq.dump_json(payload)]]
94
- end
106
+ def json(payload)
107
+ [200,
108
+ {"content-type" => "application/json", "cache-control" => "private, no-store"},
109
+ [Sidekiq.dump_json(payload)]]
110
+ end
95
111
 
96
- def initialize(env, block)
97
- @_erb = false
98
- @env = env
99
- @block = block
100
- @files ||= {}
101
- end
112
+ private
102
113
 
103
- private
114
+ def warn
115
+ Sidekiq.logger.warn yield
116
+ end
104
117
 
105
- def _erb(file, locals)
106
- locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
118
+ def _erb(file, locals)
119
+ locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
107
120
 
108
- if file.is_a?(String)
109
- ERB.new(file).result(binding)
110
- else
111
- send(:"_erb_#{file}")
121
+ if file.is_a?(String)
122
+ ERB.new(file).result(binding)
123
+ else
124
+ send(:"_erb_#{file}")
125
+ end
112
126
  end
127
+
128
+ class_eval <<-RUBY, ::Sidekiq::Web::LAYOUT, -1 # standard:disable Style/EvalWithLocation
129
+ def _render
130
+ #{ERB.new(File.read(::Sidekiq::Web::LAYOUT)).src}
131
+ end
132
+ RUBY
113
133
  end
114
134
  end
115
135
  end