sidekiq 7.3.9 → 8.0.3

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +57 -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 +93 -57
  7. data/lib/sidekiq/api.rb +122 -38
  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 +20 -16
  13. data/lib/sidekiq/embedded.rb +2 -1
  14. data/lib/sidekiq/iterable_job.rb +1 -0
  15. data/lib/sidekiq/job/iterable.rb +13 -4
  16. data/lib/sidekiq/job_logger.rb +4 -4
  17. data/lib/sidekiq/job_retry.rb +17 -5
  18. data/lib/sidekiq/job_util.rb +5 -1
  19. data/lib/sidekiq/launcher.rb +2 -1
  20. data/lib/sidekiq/logger.rb +19 -70
  21. data/lib/sidekiq/manager.rb +0 -1
  22. data/lib/sidekiq/metrics/query.rb +71 -45
  23. data/lib/sidekiq/metrics/shared.rb +8 -5
  24. data/lib/sidekiq/metrics/tracking.rb +9 -7
  25. data/lib/sidekiq/middleware/current_attributes.rb +5 -17
  26. data/lib/sidekiq/paginator.rb +8 -1
  27. data/lib/sidekiq/processor.rb +21 -14
  28. data/lib/sidekiq/profiler.rb +72 -0
  29. data/lib/sidekiq/rails.rb +43 -65
  30. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  31. data/lib/sidekiq/redis_connection.rb +14 -3
  32. data/lib/sidekiq/testing.rb +2 -2
  33. data/lib/sidekiq/version.rb +2 -2
  34. data/lib/sidekiq/web/action.rb +122 -83
  35. data/lib/sidekiq/web/application.rb +345 -332
  36. data/lib/sidekiq/web/config.rb +117 -0
  37. data/lib/sidekiq/web/helpers.rb +41 -16
  38. data/lib/sidekiq/web/router.rb +60 -76
  39. data/lib/sidekiq/web.rb +50 -156
  40. data/lib/sidekiq.rb +2 -2
  41. data/sidekiq.gemspec +6 -6
  42. data/web/assets/javascripts/application.js +6 -13
  43. data/web/assets/javascripts/base-charts.js +30 -16
  44. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  45. data/web/assets/javascripts/metrics.js +16 -34
  46. data/web/assets/stylesheets/style.css +757 -0
  47. data/web/locales/ar.yml +1 -0
  48. data/web/locales/cs.yml +1 -0
  49. data/web/locales/da.yml +1 -0
  50. data/web/locales/de.yml +1 -0
  51. data/web/locales/el.yml +1 -0
  52. data/web/locales/en.yml +6 -0
  53. data/web/locales/es.yml +24 -2
  54. data/web/locales/fa.yml +1 -0
  55. data/web/locales/fr.yml +1 -0
  56. data/web/locales/gd.yml +1 -0
  57. data/web/locales/he.yml +1 -0
  58. data/web/locales/hi.yml +1 -0
  59. data/web/locales/it.yml +8 -0
  60. data/web/locales/ja.yml +1 -0
  61. data/web/locales/ko.yml +1 -0
  62. data/web/locales/lt.yml +1 -0
  63. data/web/locales/nb.yml +1 -0
  64. data/web/locales/nl.yml +1 -0
  65. data/web/locales/pl.yml +1 -0
  66. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  67. data/web/locales/pt.yml +1 -0
  68. data/web/locales/ru.yml +1 -0
  69. data/web/locales/sv.yml +1 -0
  70. data/web/locales/ta.yml +1 -0
  71. data/web/locales/tr.yml +1 -0
  72. data/web/locales/uk.yml +1 -0
  73. data/web/locales/ur.yml +1 -0
  74. data/web/locales/vi.yml +1 -0
  75. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  76. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  77. data/web/views/_footer.erb +31 -33
  78. data/web/views/_job_info.erb +91 -89
  79. data/web/views/_metrics_period_select.erb +13 -10
  80. data/web/views/_nav.erb +14 -21
  81. data/web/views/_paging.erb +23 -21
  82. data/web/views/_poll_link.erb +2 -2
  83. data/web/views/_summary.erb +16 -16
  84. data/web/views/busy.erb +124 -122
  85. data/web/views/dashboard.erb +62 -66
  86. data/web/views/dead.erb +31 -27
  87. data/web/views/filtering.erb +3 -3
  88. data/web/views/layout.erb +13 -29
  89. data/web/views/metrics.erb +75 -81
  90. data/web/views/metrics_for_job.erb +45 -46
  91. data/web/views/morgue.erb +61 -70
  92. data/web/views/profiles.erb +43 -0
  93. data/web/views/queue.erb +54 -52
  94. data/web/views/queues.erb +43 -41
  95. data/web/views/retries.erb +66 -75
  96. data/web/views/retry.erb +32 -27
  97. data/web/views/scheduled.erb +58 -54
  98. data/web/views/scheduled_job_info.erb +1 -1
  99. metadata +24 -24
  100. data/web/assets/stylesheets/application-dark.css +0 -147
  101. data/web/assets/stylesheets/application-rtl.css +0 -163
  102. data/web/assets/stylesheets/application.css +0 -759
  103. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  104. data/web/assets/stylesheets/bootstrap.css +0 -5
  105. data/web/views/_status.erb +0 -4
@@ -0,0 +1,72 @@
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
+ @vernier_output_dir = ENV.fetch("VERNIER_OUTPUT_DIR") { Dir.tmpdir }
17
+ end
18
+
19
+ def call(job, &block)
20
+ return yield unless job["profile"]
21
+
22
+ token = job["profile"]
23
+ type = job["class"]
24
+ jid = job["jid"]
25
+ started_at = Time.now
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: File.join(
34
+ @vernier_output_dir,
35
+ "#{token}-#{type}-#{jid}-#{started_at.strftime("%Y%m%d-%H%M%S")}.json.gz"
36
+ )
37
+ }
38
+ profiler_options = profiler_options(job, rundata)
39
+
40
+ require "vernier"
41
+ begin
42
+ a = Time.now
43
+ rc = Vernier.profile(**profiler_options, &block)
44
+ b = Time.now
45
+
46
+ # Failed jobs will raise an exception on previous line and skip this
47
+ # block. Only successful jobs will persist profile data to Redis.
48
+ key = "#{token}-#{jid}"
49
+ data = File.read(rundata[:filename])
50
+ redis do |conn|
51
+ conn.multi do |m|
52
+ m.zadd("profiles", Time.now.to_f + EXPIRY, key)
53
+ m.hset(key, rundata.merge(elapsed: (b - a), data: data, size: data.bytesize))
54
+ m.expire(key, EXPIRY)
55
+ end
56
+ end
57
+ rc
58
+ ensure
59
+ FileUtils.rm_f(rundata[:filename])
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def profiler_options(job, rundata)
66
+ profiler_options = (job["profiler_options"] || {}).transform_keys(&:to_sym)
67
+ profiler_options[:mode] = profiler_options[:mode].to_sym if profiler_options[:mode]
68
+
69
+ DEFAULT_OPTIONS.merge(profiler_options, {out: rundata[:filename]})
70
+ end
71
+ end
72
+ end
data/lib/sidekiq/rails.rb CHANGED
@@ -1,84 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/job"
4
- require "rails"
4
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
5
5
 
6
6
  module Sidekiq
7
- module ActiveJob
8
- # @api private
9
- class Wrapper
10
- include Sidekiq::Job
11
-
12
- def perform(job_data)
13
- ::ActiveJob::Base.execute(job_data.merge("provider_job_id" => jid))
14
- end
15
- end
16
- end
17
-
18
- class Rails < ::Rails::Engine
19
- class Reloader
20
- def initialize(app = ::Rails.application)
21
- @app = app
22
- end
23
-
24
- def call
25
- params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
26
- @app.reloader.wrap(**params) do
27
- yield
7
+ begin
8
+ gem "railties", ">= 7.0"
9
+ require "rails"
10
+
11
+ class Rails < ::Rails::Engine
12
+ class Reloader
13
+ def initialize(app = ::Rails.application)
14
+ @app = app
28
15
  end
29
- end
30
16
 
31
- def inspect
32
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
33
- end
17
+ def call
18
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
19
+ @app.reloader.wrap(**params) do
20
+ yield
21
+ end
22
+ end
34
23
 
35
- def to_hash
36
- {app: @app.class.name}
37
- end
38
- end
24
+ def inspect
25
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
26
+ end
39
27
 
40
- # By including the Options module, we allow AJs to directly control sidekiq features
41
- # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
42
- # AJ retries don't show up in the Sidekiq UI Retries tab, don't save any error data, can't be
43
- # manually retried, don't automatically die, etc.
44
- #
45
- # class SomeJob < ActiveJob::Base
46
- # queue_as :default
47
- # sidekiq_options retry: 3, backtrace: 10
48
- # def perform
49
- # end
50
- # end
51
- initializer "sidekiq.active_job_integration" do
52
- ActiveSupport.on_load(:active_job) do
53
- require_relative "../active_job/queue_adapters/sidekiq_adapter"
54
- include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
28
+ def to_hash
29
+ {app: @app.class.name}
30
+ end
55
31
  end
56
- end
57
32
 
58
- initializer "sidekiq.backtrace_cleaner" do
59
- Sidekiq.configure_server do |config|
60
- config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
33
+ initializer "sidekiq.backtrace_cleaner" do
34
+ Sidekiq.configure_server do |config|
35
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
36
+ end
61
37
  end
62
- end
63
-
64
- # This hook happens after all initializers are run, just before returning
65
- # from config/environment.rb back to sidekiq/cli.rb.
66
- #
67
- # None of this matters on the client-side, only within the Sidekiq process itself.
68
- config.after_initialize do
69
- Sidekiq.configure_server do |config|
70
- config[:reloader] = Sidekiq::Rails::Reloader.new
71
38
 
72
- # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
73
- # it will appear in the Sidekiq console with all of the job context.
74
- unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
75
- if ::Rails.logger.respond_to?(:broadcast_to)
76
- ::Rails.logger.broadcast_to(config.logger)
77
- else
78
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
39
+ # This hook happens after all initializers are run, just before returning
40
+ # from config/environment.rb back to sidekiq/cli.rb.
41
+ #
42
+ # None of this matters on the client-side, only within the Sidekiq process itself.
43
+ config.after_initialize do
44
+ Sidekiq.configure_server do |config|
45
+ config[:reloader] = Sidekiq::Rails::Reloader.new
46
+
47
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
48
+ # it will appear in the Sidekiq console with all of the job context.
49
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
50
+ if ::Rails.logger.respond_to?(:broadcast_to)
51
+ ::Rails.logger.broadcast_to(config.logger)
52
+ else
53
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
54
+ end
79
55
  end
80
56
  end
81
57
  end
82
58
  end
59
+ rescue Gem::LoadError
60
+ # Rails not available or version requirement not met
83
61
  end
84
62
  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
 
@@ -10,6 +10,8 @@ module Sidekiq
10
10
  def create(options = {})
11
11
  symbolized_options = deep_symbolize_keys(options)
12
12
  symbolized_options[:url] ||= determine_redis_provider
13
+ symbolized_options[:password] = wrap(symbolized_options[:password]) if symbolized_options.key?(:password)
14
+ symbolized_options[:sentinel_password] = wrap(symbolized_options[:sentinel_password]) if symbolized_options.key?(:sentinel_password)
13
15
 
14
16
  logger = symbolized_options.delete(:logger)
15
17
  logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
@@ -38,6 +40,15 @@ module Sidekiq
38
40
 
39
41
  private
40
42
 
43
+ # Wrap hard-coded passwords in a Proc to avoid logging the value
44
+ def wrap(pwd)
45
+ if pwd.is_a?(String)
46
+ ->(username) { pwd }
47
+ else
48
+ pwd
49
+ end
50
+ end
51
+
41
52
  def deep_symbolize_keys(object)
42
53
  case object
43
54
  when Hash
@@ -57,14 +68,14 @@ module Sidekiq
57
68
  # Deep clone so we can muck with these options all we want and exclude
58
69
  # params from dump-and-load that may contain objects that Marshal is
59
70
  # unable to safely dump.
60
- keys = options.keys - [:logger, :ssl_params]
71
+ keys = options.keys - [:logger, :ssl_params, :password, :sentinel_password]
61
72
  scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
62
73
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
63
74
  uri.password = redacted
64
75
  scrubbed_options[:url] = uri.to_s
65
76
  end
66
- scrubbed_options[:password] = redacted if scrubbed_options[:password]
67
- scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
77
+ scrubbed_options[:password] = redacted if options.key?(:password)
78
+ scrubbed_options[:sentinel_password] = redacted if options.key?(:sentinel_password)
68
79
  scrubbed_options[:sentinels]&.each do |sentinel|
69
80
  if sentinel.is_a?(String)
70
81
  if (uri = URI(sentinel)) && uri.password
@@ -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.3"
5
+ MAJOR = 8
6
6
 
7
7
  def self.gem_version
8
8
  Gem::Version.new(VERSION)
@@ -1,115 +1,154 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "erb"
4
+
3
5
  module Sidekiq
4
- class WebAction
5
- RACK_SESSION = "rack.session"
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
6
18
 
7
- attr_accessor :env, :block, :type
19
+ def config
20
+ env[:web_config]
21
+ end
8
22
 
9
- def settings
10
- Web.settings
11
- end
23
+ def request
24
+ @request ||= ::Rack::Request.new(env)
25
+ end
12
26
 
13
- def request
14
- @request ||= ::Rack::Request.new(env)
15
- end
27
+ def halt(res)
28
+ throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
29
+ end
16
30
 
17
- def halt(res)
18
- throw :halt, [res, {Rack::CONTENT_TYPE => "text/plain"}, [res.to_s]]
19
- end
31
+ # external redirect
32
+ def redirect_to(url)
33
+ throw :halt, [302, {"location" => url}, []]
34
+ end
20
35
 
21
- def redirect(location)
22
- throw :halt, [302, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
23
- end
36
+ def header(key, value)
37
+ env["response_headers"][key] = value.to_s
38
+ end
24
39
 
25
- def reload_page
26
- current_location = request.referer.gsub(request.base_url, "")
27
- redirect current_location
28
- end
40
+ # internal redirect
41
+ def redirect(location)
42
+ throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
43
+ end
29
44
 
30
- # deprecated, will warn in 8.0
31
- def params
32
- indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
45
+ def reload_page
46
+ current_location = request.referer.gsub(request.base_url, "")
47
+ redirect current_location
48
+ end
33
49
 
34
- indifferent_hash.merge! request.params
35
- route_params.each { |k, v| indifferent_hash[k.to_s] = v }
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
36
56
 
37
- indifferent_hash
38
- 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
39
63
 
40
- # Use like `url_params("page")` within your action blocks
41
- def url_params(key)
42
- request.params[key]
43
- end
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
67
+ end
44
68
 
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]
69
+ def session
70
+ env["rack.session"]
52
71
  end
53
- end
54
72
 
55
- def session
56
- env[RACK_SESSION]
57
- end
73
+ def logger
74
+ Sidekiq.logger
75
+ end
58
76
 
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
- end
77
+ # flash { "Some message to show on redirect" }
78
+ def flash
79
+ msg = yield
80
+ logger.info msg
81
+ session[:flash] = msg
74
82
  end
75
83
 
76
- if @_erb
77
- _erb(content, options[:locals])
78
- else
79
- @_erb = true
80
- content = _erb(content, options[:locals])
84
+ def flash?
85
+ session&.[](:flash)
86
+ end
81
87
 
82
- _render { content }
88
+ def get_flash
89
+ @flash ||= session.delete(:flash)
83
90
  end
84
- end
85
91
 
86
- def render(engine, content, options = {})
87
- raise "Only erb templates are supported" if engine != :erb
92
+ def erb(content, options = {})
93
+ if content.is_a? Symbol
94
+ unless respond_to?(:"_erb_#{content}")
95
+ views = options[:views] || Web.views
96
+ filename = "#{views}/#{content}.erb"
97
+ src = ERB.new(File.read(filename)).src
98
+
99
+ # Need to use lineno less by 1 because erb generates a
100
+ # comment before the source code.
101
+ Action.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
102
+ def _erb_#{content}
103
+ #{src}
104
+ end
105
+ RUBY
106
+ end
107
+ end
88
108
 
89
- erb(content, options)
90
- end
109
+ if @_erb
110
+ _erb(content, options[:locals])
111
+ else
112
+ @_erb = true
113
+ content = _erb(content, options[:locals])
91
114
 
92
- def json(payload)
93
- [200, {Rack::CONTENT_TYPE => "application/json", Rack::CACHE_CONTROL => "private, no-store"}, [Sidekiq.dump_json(payload)]]
94
- end
115
+ _render { content }
116
+ end
117
+ end
95
118
 
96
- def initialize(env, block)
97
- @_erb = false
98
- @env = env
99
- @block = block
100
- @files ||= {}
101
- end
119
+ def render(engine, content, options = {})
120
+ raise "Only erb templates are supported" if engine != :erb
121
+
122
+ erb(content, options)
123
+ end
124
+
125
+ def json(payload)
126
+ [200,
127
+ {"content-type" => "application/json", "cache-control" => "private, no-store"},
128
+ [Sidekiq.dump_json(payload)]]
129
+ end
102
130
 
103
- private
131
+ private
104
132
 
105
- def _erb(file, locals)
106
- locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
133
+ def warn
134
+ Sidekiq.logger.warn yield
135
+ end
136
+
137
+ def _erb(file, locals)
138
+ locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
107
139
 
108
- if file.is_a?(String)
109
- ERB.new(file).result(binding)
110
- else
111
- send(:"_erb_#{file}")
140
+ if file.is_a?(String)
141
+ ERB.new(file).result(binding)
142
+ else
143
+ send(:"_erb_#{file}")
144
+ end
112
145
  end
146
+
147
+ class_eval <<-RUBY, ::Sidekiq::Web::LAYOUT, -1 # standard:disable Style/EvalWithLocation
148
+ def _render
149
+ #{ERB.new(File.read(::Sidekiq::Web::LAYOUT)).src}
150
+ end
151
+ RUBY
113
152
  end
114
153
  end
115
154
  end