sidekiq 7.2.4 → 7.3.0
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.
- checksums.yaml +4 -4
- data/Changes.md +35 -0
- data/README.md +1 -1
- data/lib/sidekiq/api.rb +1 -1
- data/lib/sidekiq/capsule.rb +3 -0
- data/lib/sidekiq/cli.rb +1 -0
- data/lib/sidekiq/client.rb +2 -2
- data/lib/sidekiq/config.rb +5 -1
- data/lib/sidekiq/fetch.rb +1 -1
- data/lib/sidekiq/iterable_job.rb +53 -0
- data/lib/sidekiq/job/interrupt_handler.rb +22 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/lib/sidekiq/job/iterable.rb +231 -0
- data/lib/sidekiq/job.rb +13 -2
- data/lib/sidekiq/job_logger.rb +23 -10
- data/lib/sidekiq/job_retry.rb +6 -1
- data/lib/sidekiq/middleware/current_attributes.rb +27 -11
- data/lib/sidekiq/processor.rb +11 -1
- data/lib/sidekiq/redis_client_adapter.rb +8 -5
- data/lib/sidekiq/redis_connection.rb +25 -1
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +2 -1
- data/lib/sidekiq/web/application.rb +8 -4
- data/lib/sidekiq/web/helpers.rb +53 -7
- data/lib/sidekiq/web.rb +46 -1
- data/lib/sidekiq.rb +2 -1
- data/sidekiq.gemspec +2 -1
- data/web/assets/javascripts/application.js +6 -1
- data/web/assets/javascripts/dashboard-charts.js +22 -12
- data/web/assets/javascripts/dashboard.js +1 -1
- data/web/assets/stylesheets/application.css +13 -1
- data/web/locales/tr.yml +101 -0
- data/web/views/dashboard.erb +6 -6
- data/web/views/layout.erb +6 -6
- data/web/views/metrics.erb +4 -4
- data/web/views/metrics_for_job.erb +4 -4
- metadata +26 -5
data/lib/sidekiq/job.rb
CHANGED
@@ -69,7 +69,11 @@ module Sidekiq
|
|
69
69
|
# In practice, any option is allowed. This is the main mechanism to configure the
|
70
70
|
# options for a specific job.
|
71
71
|
def sidekiq_options(opts = {})
|
72
|
-
|
72
|
+
# stringify 2 levels of keys
|
73
|
+
opts = opts.to_h do |k, v|
|
74
|
+
[k.to_s, (Hash === v) ? v.transform_keys(&:to_s) : v]
|
75
|
+
end
|
76
|
+
|
73
77
|
self.sidekiq_options_hash = get_sidekiq_options.merge(opts)
|
74
78
|
end
|
75
79
|
|
@@ -155,6 +159,9 @@ module Sidekiq
|
|
155
159
|
|
156
160
|
attr_accessor :jid
|
157
161
|
|
162
|
+
# This attribute is implementation-specific and not a public API
|
163
|
+
attr_accessor :_context
|
164
|
+
|
158
165
|
def self.included(base)
|
159
166
|
raise ArgumentError, "Sidekiq::Job cannot be included in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
|
160
167
|
|
@@ -166,6 +173,10 @@ module Sidekiq
|
|
166
173
|
Sidekiq.logger
|
167
174
|
end
|
168
175
|
|
176
|
+
def interrupted?
|
177
|
+
@_context&.stopping?
|
178
|
+
end
|
179
|
+
|
169
180
|
# This helper class encapsulates the set options for `set`, e.g.
|
170
181
|
#
|
171
182
|
# SomeJob.set(queue: 'foo').perform_async(....)
|
@@ -366,7 +377,7 @@ module Sidekiq
|
|
366
377
|
|
367
378
|
def build_client # :nodoc:
|
368
379
|
pool = Thread.current[:sidekiq_redis_pool] || get_sidekiq_options["pool"] || Sidekiq.default_configuration.redis_pool
|
369
|
-
client_class = get_sidekiq_options["client_class"] || Sidekiq::Client
|
380
|
+
client_class = Thread.current[:sidekiq_client_class] || get_sidekiq_options["client_class"] || Sidekiq::Client
|
370
381
|
client_class.new(pool: pool)
|
371
382
|
end
|
372
383
|
end
|
data/lib/sidekiq/job_logger.rb
CHANGED
@@ -2,23 +2,36 @@
|
|
2
2
|
|
3
3
|
module Sidekiq
|
4
4
|
class JobLogger
|
5
|
-
|
5
|
+
include Sidekiq::Component
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
6
9
|
@logger = logger
|
7
10
|
end
|
8
11
|
|
12
|
+
# If true we won't do any job logging out of the box.
|
13
|
+
# The user is responsible for any logging.
|
14
|
+
def skip_default_logging?
|
15
|
+
config[:skip_default_job_logging]
|
16
|
+
end
|
17
|
+
|
9
18
|
def call(item, queue)
|
10
|
-
|
11
|
-
@logger.info("start")
|
19
|
+
return yield if skip_default_logging?
|
12
20
|
|
13
|
-
|
21
|
+
begin
|
22
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
23
|
+
@logger.info("start")
|
14
24
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
25
|
+
yield
|
26
|
+
|
27
|
+
Sidekiq::Context.add(:elapsed, elapsed(start))
|
28
|
+
@logger.info("done")
|
29
|
+
rescue Exception
|
30
|
+
Sidekiq::Context.add(:elapsed, elapsed(start))
|
31
|
+
@logger.info("fail")
|
20
32
|
|
21
|
-
|
33
|
+
raise
|
34
|
+
end
|
22
35
|
end
|
23
36
|
|
24
37
|
def prepare(job_hash, &block)
|
data/lib/sidekiq/job_retry.rb
CHANGED
@@ -59,8 +59,13 @@ module Sidekiq
|
|
59
59
|
# end
|
60
60
|
#
|
61
61
|
class JobRetry
|
62
|
+
# Handled means the job failed but has been dealt with
|
63
|
+
# (by creating a retry, rescheduling it, etc). It still
|
64
|
+
# needs to be logged and dispatched to error_handlers.
|
62
65
|
class Handled < ::RuntimeError; end
|
63
66
|
|
67
|
+
# Skip means the job failed but Sidekiq does not need to
|
68
|
+
# create a retry, log it or send to error_handlers.
|
64
69
|
class Skip < Handled; end
|
65
70
|
|
66
71
|
include Sidekiq::Component
|
@@ -129,7 +134,7 @@ module Sidekiq
|
|
129
134
|
process_retry(jobinst, msg, queue, e)
|
130
135
|
# We've handled this error associated with this job, don't
|
131
136
|
# need to handle it at the global level
|
132
|
-
raise
|
137
|
+
raise Handled
|
133
138
|
end
|
134
139
|
|
135
140
|
private
|
@@ -46,22 +46,38 @@ module Sidekiq
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def call(_, job, _, &block)
|
49
|
-
|
49
|
+
klass_attrs = {}
|
50
50
|
|
51
51
|
@cattrs.each do |(key, strklass)|
|
52
|
-
|
53
|
-
constklass = strklass.constantize
|
54
|
-
cattrs_to_reset << constklass
|
52
|
+
next unless job.has_key?(key)
|
55
53
|
|
56
|
-
|
57
|
-
constklass.public_send(:"#{attribute}=", value)
|
58
|
-
end
|
59
|
-
end
|
54
|
+
klass_attrs[strklass.constantize] = job[key]
|
60
55
|
end
|
61
56
|
|
62
|
-
|
63
|
-
|
64
|
-
|
57
|
+
wrap(klass_attrs.to_a, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def wrap(klass_attrs, &block)
|
63
|
+
klass, attrs = klass_attrs.shift
|
64
|
+
return block.call unless klass
|
65
|
+
|
66
|
+
retried = false
|
67
|
+
|
68
|
+
begin
|
69
|
+
klass.set(attrs) do
|
70
|
+
wrap(klass_attrs, &block)
|
71
|
+
end
|
72
|
+
rescue NoMethodError
|
73
|
+
raise if retried
|
74
|
+
|
75
|
+
# It is possible that the `CurrentAttributes` definition
|
76
|
+
# was changed before the job started processing.
|
77
|
+
attrs = attrs.select { |attr| klass.respond_to?(attr) }
|
78
|
+
retried = true
|
79
|
+
retry
|
80
|
+
end
|
65
81
|
end
|
66
82
|
end
|
67
83
|
|
data/lib/sidekiq/processor.rb
CHANGED
@@ -36,7 +36,7 @@ module Sidekiq
|
|
36
36
|
@job = nil
|
37
37
|
@thread = nil
|
38
38
|
@reloader = Sidekiq.default_configuration[:reloader]
|
39
|
-
@job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(
|
39
|
+
@job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
|
40
40
|
@retrier = Sidekiq::JobRetry.new(capsule)
|
41
41
|
end
|
42
42
|
|
@@ -58,6 +58,10 @@ module Sidekiq
|
|
58
58
|
@thread.value if wait
|
59
59
|
end
|
60
60
|
|
61
|
+
def stopping?
|
62
|
+
@done
|
63
|
+
end
|
64
|
+
|
61
65
|
def start
|
62
66
|
@thread ||= safe_thread("#{config.name}/processor", &method(:run))
|
63
67
|
end
|
@@ -136,6 +140,7 @@ module Sidekiq
|
|
136
140
|
klass = Object.const_get(job_hash["class"])
|
137
141
|
inst = klass.new
|
138
142
|
inst.jid = job_hash["jid"]
|
143
|
+
inst._context = self
|
139
144
|
@retrier.local(inst, jobstr, queue) do
|
140
145
|
yield inst
|
141
146
|
end
|
@@ -185,6 +190,11 @@ module Sidekiq
|
|
185
190
|
# Had to force kill this job because it didn't finish
|
186
191
|
# within the timeout. Don't acknowledge the work since
|
187
192
|
# we didn't properly finish it.
|
193
|
+
rescue Sidekiq::JobRetry::Skip => s
|
194
|
+
# Skip means we handled this error elsewhere. We don't
|
195
|
+
# need to log or report the error.
|
196
|
+
ack = true
|
197
|
+
raise s
|
188
198
|
rescue Sidekiq::JobRetry::Handled => h
|
189
199
|
# this is the common case: job raised error and Sidekiq::JobRetry::Handled
|
190
200
|
# signals that we created a retry successfully. We can acknowledge the job.
|
@@ -64,6 +64,13 @@ module Sidekiq
|
|
64
64
|
opts = client_opts(options)
|
65
65
|
@config = if opts.key?(:sentinels)
|
66
66
|
RedisClient.sentinel(**opts)
|
67
|
+
elsif opts.key?(:nodes)
|
68
|
+
# Sidekiq does not support Redis clustering but Sidekiq Enterprise's
|
69
|
+
# rate limiters are cluster-safe so we can scale to millions
|
70
|
+
# of rate limiters using a Redis cluster. This requires the
|
71
|
+
# `redis-cluster-client` gem.
|
72
|
+
# Sidekiq::Limiter.redis = { nodes: [...] }
|
73
|
+
RedisClient.cluster(**opts)
|
67
74
|
else
|
68
75
|
RedisClient.config(**opts)
|
69
76
|
end
|
@@ -90,13 +97,9 @@ module Sidekiq
|
|
90
97
|
opts.delete(:network_timeout)
|
91
98
|
end
|
92
99
|
|
93
|
-
if opts[:driver]
|
94
|
-
opts[:driver] = opts[:driver].to_sym
|
95
|
-
end
|
96
|
-
|
97
100
|
opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
|
98
101
|
opts[:role] = opts[:role].to_sym if opts.key?(:role)
|
99
|
-
opts
|
102
|
+
opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
|
100
103
|
|
101
104
|
# Issue #3303, redis-rb will silently retry an operation.
|
102
105
|
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
@@ -8,17 +8,28 @@ module Sidekiq
|
|
8
8
|
module RedisConnection
|
9
9
|
class << self
|
10
10
|
def create(options = {})
|
11
|
-
symbolized_options = options
|
11
|
+
symbolized_options = deep_symbolize_keys(options)
|
12
12
|
symbolized_options[:url] ||= determine_redis_provider
|
13
13
|
|
14
14
|
logger = symbolized_options.delete(:logger)
|
15
15
|
logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
|
16
16
|
|
17
17
|
raise "Sidekiq 7+ does not support Redis protocol 2" if symbolized_options[:protocol] == 2
|
18
|
+
|
19
|
+
safe = !!symbolized_options.delete(:cluster_safe)
|
20
|
+
raise ":nodes not allowed, Sidekiq is not safe to run on Redis Cluster" if !safe && symbolized_options.key?(:nodes)
|
21
|
+
|
18
22
|
size = symbolized_options.delete(:size) || 5
|
19
23
|
pool_timeout = symbolized_options.delete(:pool_timeout) || 1
|
20
24
|
pool_name = symbolized_options.delete(:pool_name)
|
21
25
|
|
26
|
+
# Default timeout in redis-client is 1 second, which can be too aggressive
|
27
|
+
# if the Sidekiq process is CPU-bound. With 10-15 threads and a thread quantum of 100ms,
|
28
|
+
# it can be easy to get the occasional ReadTimeoutError. You can still provide
|
29
|
+
# a smaller timeout explicitly:
|
30
|
+
# config.redis = { url: "...", timeout: 1 }
|
31
|
+
symbolized_options[:timeout] ||= 3
|
32
|
+
|
22
33
|
redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
|
23
34
|
ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
|
24
35
|
redis_config.new_client
|
@@ -27,6 +38,19 @@ module Sidekiq
|
|
27
38
|
|
28
39
|
private
|
29
40
|
|
41
|
+
def deep_symbolize_keys(object)
|
42
|
+
case object
|
43
|
+
when Hash
|
44
|
+
object.each_with_object({}) do |(key, value), result|
|
45
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
46
|
+
end
|
47
|
+
when Array
|
48
|
+
object.map { |e| deep_symbolize_keys(e) }
|
49
|
+
else
|
50
|
+
object
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
30
54
|
def scrub(options)
|
31
55
|
redacted = "REDACTED"
|
32
56
|
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
@@ -47,7 +47,8 @@ module Sidekiq
|
|
47
47
|
def erb(content, options = {})
|
48
48
|
if content.is_a? Symbol
|
49
49
|
unless respond_to?(:"_erb_#{content}")
|
50
|
-
|
50
|
+
views = options[:views] || Web.settings.views
|
51
|
+
src = ERB.new(File.read("#{views}/#{content}.erb")).src
|
51
52
|
WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
52
53
|
def _erb_#{content}
|
53
54
|
#{src}
|
@@ -5,7 +5,7 @@ module Sidekiq
|
|
5
5
|
extend WebRouter
|
6
6
|
|
7
7
|
REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
|
8
|
-
|
8
|
+
CSP_HEADER_TEMPLATE = [
|
9
9
|
"default-src 'self' https: http:",
|
10
10
|
"child-src 'self'",
|
11
11
|
"connect-src 'self' https: http: wss: ws:",
|
@@ -15,8 +15,8 @@ module Sidekiq
|
|
15
15
|
"manifest-src 'self'",
|
16
16
|
"media-src 'self'",
|
17
17
|
"object-src 'none'",
|
18
|
-
"script-src 'self'
|
19
|
-
"style-src 'self' https: http: 'unsafe-inline'",
|
18
|
+
"script-src 'self' 'nonce-!placeholder!'",
|
19
|
+
"style-src 'self' https: http: 'unsafe-inline'", # TODO Nonce in 8.0
|
20
20
|
"worker-src 'self'",
|
21
21
|
"base-uri 'self'"
|
22
22
|
].join("; ").freeze
|
@@ -428,13 +428,17 @@ module Sidekiq
|
|
428
428
|
Rack::CONTENT_TYPE => "text/html",
|
429
429
|
Rack::CACHE_CONTROL => "private, no-store",
|
430
430
|
Web::CONTENT_LANGUAGE => action.locale,
|
431
|
-
Web::CONTENT_SECURITY_POLICY =>
|
431
|
+
Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE)
|
432
432
|
}
|
433
433
|
# we'll let Rack calculate Content-Length for us.
|
434
434
|
[200, headers, [resp]]
|
435
435
|
end
|
436
436
|
end
|
437
437
|
|
438
|
+
def process_csp(env, input)
|
439
|
+
input.gsub("!placeholder!", env[:csp_nonce])
|
440
|
+
end
|
441
|
+
|
438
442
|
def self.helpers(mod = nil, &block)
|
439
443
|
if block
|
440
444
|
WebAction.class_eval(&block)
|
data/lib/sidekiq/web/helpers.rb
CHANGED
@@ -6,8 +6,51 @@ require "yaml"
|
|
6
6
|
require "cgi"
|
7
7
|
|
8
8
|
module Sidekiq
|
9
|
-
#
|
9
|
+
# These methods are available to pages within the Web UI and UI extensions.
|
10
|
+
# They are not public APIs for applications to use.
|
10
11
|
module WebHelpers
|
12
|
+
def style_tag(location, **kwargs)
|
13
|
+
global = location.match?(/:\/\//)
|
14
|
+
location = root_path + location if !global && !location.start_with?(root_path)
|
15
|
+
attrs = {
|
16
|
+
type: "text/css",
|
17
|
+
media: "screen",
|
18
|
+
rel: "stylesheet",
|
19
|
+
nonce: csp_nonce,
|
20
|
+
href: location
|
21
|
+
}
|
22
|
+
html_tag(:link, attrs.merge(kwargs))
|
23
|
+
end
|
24
|
+
|
25
|
+
def script_tag(location, **kwargs)
|
26
|
+
global = location.match?(/:\/\//)
|
27
|
+
location = root_path + location if !global && !location.start_with?(root_path)
|
28
|
+
attrs = {
|
29
|
+
type: "text/javascript",
|
30
|
+
nonce: csp_nonce,
|
31
|
+
src: location
|
32
|
+
}
|
33
|
+
html_tag(:script, attrs.merge(kwargs)) {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# NB: keys and values are not escaped; do not allow user input
|
37
|
+
# in the attributes
|
38
|
+
private def html_tag(tagname, attrs)
|
39
|
+
s = "<#{tagname}"
|
40
|
+
attrs.each_pair do |k, v|
|
41
|
+
next unless v
|
42
|
+
s << " #{k}=\"#{v}\""
|
43
|
+
end
|
44
|
+
if block_given?
|
45
|
+
s << ">"
|
46
|
+
yield s
|
47
|
+
s << "</#{tagname}>"
|
48
|
+
else
|
49
|
+
s << " />"
|
50
|
+
end
|
51
|
+
s
|
52
|
+
end
|
53
|
+
|
11
54
|
def strings(lang)
|
12
55
|
@strings ||= {}
|
13
56
|
|
@@ -46,7 +89,7 @@ module Sidekiq
|
|
46
89
|
end
|
47
90
|
|
48
91
|
def available_locales
|
49
|
-
@available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }
|
92
|
+
@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
|
50
93
|
end
|
51
94
|
|
52
95
|
def find_locale_files(lang)
|
@@ -122,10 +165,9 @@ module Sidekiq
|
|
122
165
|
# Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
|
123
166
|
def locale
|
124
167
|
# session[:locale] is set via the locale selector from the footer
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
@locale ||= begin
|
168
|
+
@locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
|
169
|
+
l
|
170
|
+
else
|
129
171
|
matched_locale = user_preferred_languages.map { |preferred|
|
130
172
|
preferred_language = preferred.split("-", 2).first
|
131
173
|
|
@@ -267,6 +309,10 @@ module Sidekiq
|
|
267
309
|
"<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
|
268
310
|
end
|
269
311
|
|
312
|
+
def csp_nonce
|
313
|
+
env[:csp_nonce]
|
314
|
+
end
|
315
|
+
|
270
316
|
def to_display(arg)
|
271
317
|
arg.inspect
|
272
318
|
rescue
|
@@ -310,7 +356,7 @@ module Sidekiq
|
|
310
356
|
end
|
311
357
|
|
312
358
|
def h(text)
|
313
|
-
::Rack::Utils.escape_html(text)
|
359
|
+
::Rack::Utils.escape_html(text.to_s)
|
314
360
|
rescue ArgumentError => e
|
315
361
|
raise unless e.message.eql?("invalid byte sequence in UTF-8")
|
316
362
|
text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
|
data/lib/sidekiq/web.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "erb"
|
4
|
+
require "securerandom"
|
4
5
|
|
5
6
|
require "sidekiq"
|
6
7
|
require "sidekiq/api"
|
@@ -114,6 +115,7 @@ module Sidekiq
|
|
114
115
|
end
|
115
116
|
|
116
117
|
def call(env)
|
118
|
+
env[:csp_nonce] = SecureRandom.base64(16)
|
117
119
|
app.call(env)
|
118
120
|
end
|
119
121
|
|
@@ -138,7 +140,50 @@ module Sidekiq
|
|
138
140
|
send(:"#{attribute}=", value)
|
139
141
|
end
|
140
142
|
|
141
|
-
|
143
|
+
# Register a class as a Sidekiq Web UI extension. The class should
|
144
|
+
# provide one or more tabs which map to an index route. Options:
|
145
|
+
#
|
146
|
+
# @param extension [Class] Class which contains the HTTP actions, required
|
147
|
+
# @param name [String] the name of the extension, used to namespace assets
|
148
|
+
# @param tab [String | Array] labels(s) of the UI tabs
|
149
|
+
# @param index [String | Array] index route(s) for each tab
|
150
|
+
# @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
|
151
|
+
# @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
|
152
|
+
# @param cache_for [Integer] amount of time to cache assets, default one day
|
153
|
+
#
|
154
|
+
# TODO name, tab and index will be mandatory in 8.0
|
155
|
+
#
|
156
|
+
# Web extensions will have a root `web/` directory with `locales/`, `assets/`
|
157
|
+
# and `views/` subdirectories.
|
158
|
+
def self.register(extension, name: nil, tab: nil, index: nil, root_dir: nil, cache_for: 86400, asset_paths: nil)
|
159
|
+
tab = Array(tab)
|
160
|
+
index = Array(index)
|
161
|
+
tab.zip(index).each do |tab, index|
|
162
|
+
tabs[tab] = index
|
163
|
+
end
|
164
|
+
if root_dir
|
165
|
+
locdir = File.join(root_dir, "locales")
|
166
|
+
locales << locdir if File.directory?(locdir)
|
167
|
+
|
168
|
+
if asset_paths && name
|
169
|
+
# if you have {root}/assets/{name}/js/scripts.js
|
170
|
+
# and {root}/assets/{name}/css/styles.css
|
171
|
+
# you would pass in:
|
172
|
+
# asset_paths: ["js", "css"]
|
173
|
+
# See script_tag and style_tag in web/helpers.rb
|
174
|
+
assdir = File.join(root_dir, "assets")
|
175
|
+
assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
|
176
|
+
assetprops = {
|
177
|
+
urls: assurls,
|
178
|
+
root: assdir,
|
179
|
+
cascade: true
|
180
|
+
}
|
181
|
+
assetprops[:header_rules] = [[:all, {Rack::CACHE_CONTROL => "private, max-age=#{cache_for.to_i}"}]] if cache_for
|
182
|
+
middlewares << [[Rack::Static, assetprops], nil]
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
yield self if block_given?
|
142
187
|
extension.registered(WebApplication)
|
143
188
|
end
|
144
189
|
|
data/lib/sidekiq.rb
CHANGED
@@ -32,6 +32,7 @@ require "sidekiq/logger"
|
|
32
32
|
require "sidekiq/client"
|
33
33
|
require "sidekiq/transaction_aware_client"
|
34
34
|
require "sidekiq/job"
|
35
|
+
require "sidekiq/iterable_job"
|
35
36
|
require "sidekiq/worker_compatibility_alias"
|
36
37
|
require "sidekiq/redis_client_adapter"
|
37
38
|
|
@@ -112,7 +113,7 @@ module Sidekiq
|
|
112
113
|
# end
|
113
114
|
# inst.run
|
114
115
|
# sleep 10
|
115
|
-
# inst.
|
116
|
+
# inst.stop
|
116
117
|
#
|
117
118
|
# NB: it is really easy to overload a Ruby process with threads due to the GIL.
|
118
119
|
# I do not recommend setting concurrency higher than 2-3.
|
data/sidekiq.gemspec
CHANGED
@@ -23,8 +23,9 @@ Gem::Specification.new do |gem|
|
|
23
23
|
"rubygems_mfa_required" => "true"
|
24
24
|
}
|
25
25
|
|
26
|
-
gem.add_dependency "redis-client", ">= 0.
|
26
|
+
gem.add_dependency "redis-client", ">= 0.22.2"
|
27
27
|
gem.add_dependency "connection_pool", ">= 2.3.0"
|
28
28
|
gem.add_dependency "rack", ">= 2.2.4"
|
29
29
|
gem.add_dependency "concurrent-ruby", "< 2"
|
30
|
+
gem.add_dependency "logger"
|
30
31
|
end
|
@@ -34,6 +34,7 @@ function addListeners() {
|
|
34
34
|
addShiftClickListeners()
|
35
35
|
updateFuzzyTimes();
|
36
36
|
updateNumbers();
|
37
|
+
updateProgressBars();
|
37
38
|
setLivePollFromUrl();
|
38
39
|
|
39
40
|
var buttons = document.querySelectorAll(".live-poll");
|
@@ -180,4 +181,8 @@ function showError(error) {
|
|
180
181
|
|
181
182
|
function updateLocale(event) {
|
182
183
|
event.target.form.submit();
|
183
|
-
}
|
184
|
+
}
|
185
|
+
|
186
|
+
function updateProgressBars() {
|
187
|
+
document.querySelectorAll('.progress-bar').forEach(bar => { bar.style.width = bar.dataset.width + "%"})
|
188
|
+
}
|
@@ -108,17 +108,27 @@ class RealtimeChart extends DashboardChart {
|
|
108
108
|
}
|
109
109
|
|
110
110
|
renderLegend(dp) {
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
111
|
+
const entry1 = this.legendEntry(dp[0]);
|
112
|
+
const entry2 = this.legendEntry(dp[1]);
|
113
|
+
const time = document.createElement("span");
|
114
|
+
time.classList.add("time");
|
115
|
+
time.innerText = dp[0].label;
|
116
|
+
|
117
|
+
this.legend.replaceChildren(entry1, entry2, time)
|
118
|
+
}
|
119
|
+
|
120
|
+
legendEntry(dp) {
|
121
|
+
const wrapper = document.createElement("span");
|
122
|
+
|
123
|
+
const swatch = document.createElement("span");
|
124
|
+
swatch.classList.add("swatch");
|
125
|
+
swatch.style.backgroundColor = dp.dataset.borderColor;
|
126
|
+
wrapper.appendChild(swatch)
|
127
|
+
|
128
|
+
const label = document.createElement("span");
|
129
|
+
label.innerText = `${dp.dataset.label}: ${dp.formattedValue}`;
|
130
|
+
wrapper.appendChild(label)
|
131
|
+
return wrapper;
|
122
132
|
}
|
123
133
|
|
124
134
|
renderCursor(dp) {
|
@@ -179,4 +189,4 @@ class RealtimeChart extends DashboardChart {
|
|
179
189
|
if (hc != null) {
|
180
190
|
var htc = new DashboardChart(hc, JSON.parse(hc.textContent))
|
181
191
|
window.historyChart = htc
|
182
|
-
}
|
192
|
+
}
|
@@ -28,7 +28,7 @@ var pulseBeacon = function() {
|
|
28
28
|
}
|
29
29
|
|
30
30
|
var setSliderLabel = function(val) {
|
31
|
-
document.getElementById('sldr-text').innerText = Math.round(parseFloat(val) / 1000) + '
|
31
|
+
document.getElementById('sldr-text').innerText = Math.round(parseFloat(val) / 1000) + ' s';
|
32
32
|
}
|
33
33
|
|
34
34
|
var ready = (callback) => {
|
@@ -72,6 +72,14 @@ h1, h2, h3 {
|
|
72
72
|
line-height: 45px;
|
73
73
|
}
|
74
74
|
|
75
|
+
.progress {
|
76
|
+
margin-bottom: 0;
|
77
|
+
}
|
78
|
+
|
79
|
+
.w-50 {
|
80
|
+
width: 50%;
|
81
|
+
}
|
82
|
+
|
75
83
|
.header-container, .header-container .page-title-container {
|
76
84
|
display: flex;
|
77
85
|
justify-content: space-between;
|
@@ -626,8 +634,12 @@ div.interval-slider input {
|
|
626
634
|
.container {
|
627
635
|
padding: 0;
|
628
636
|
}
|
637
|
+
.navbar-fixed-bottom {
|
638
|
+
position: relative;
|
639
|
+
top: auto;
|
640
|
+
}
|
629
641
|
@media (max-width: 767px) {
|
630
|
-
.navbar-fixed-top
|
642
|
+
.navbar-fixed-top {
|
631
643
|
position: relative;
|
632
644
|
top: auto;
|
633
645
|
}
|