sidekiq 8.0.2 → 8.1.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 +108 -0
- data/README.md +15 -0
- data/bin/lint-herb +13 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +104 -58
- data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
- data/lib/sidekiq/api.rb +30 -6
- data/lib/sidekiq/capsule.rb +4 -0
- data/lib/sidekiq/cli.rb +16 -4
- data/lib/sidekiq/client.rb +15 -1
- data/lib/sidekiq/component.rb +2 -1
- data/lib/sidekiq/config.rb +11 -6
- data/lib/sidekiq/fetch.rb +1 -0
- data/lib/sidekiq/job/iterable.rb +33 -14
- data/lib/sidekiq/job.rb +4 -2
- data/lib/sidekiq/job_logger.rb +5 -3
- data/lib/sidekiq/job_retry.rb +23 -8
- data/lib/sidekiq/launcher.rb +18 -9
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +16 -9
- data/lib/sidekiq/metrics/tracking.rb +3 -0
- data/lib/sidekiq/middleware/current_attributes.rb +6 -2
- data/lib/sidekiq/middleware/i18n.rb +2 -0
- data/lib/sidekiq/monitor.rb +4 -8
- data/lib/sidekiq/profiler.rb +17 -3
- data/lib/sidekiq/rails.rb +46 -67
- data/lib/sidekiq/redis_connection.rb +2 -2
- data/lib/sidekiq/ring_buffer.rb +1 -0
- data/lib/sidekiq/scheduled.rb +7 -5
- data/lib/sidekiq/testing.rb +1 -1
- data/lib/sidekiq/transaction_aware_client.rb +13 -5
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +45 -2
- data/lib/sidekiq/web/application.rb +22 -4
- data/lib/sidekiq/web/config.rb +3 -3
- data/lib/sidekiq/web/helpers.rb +26 -29
- data/lib/sidekiq/web.rb +23 -3
- data/lib/sidekiq.rb +5 -0
- data/sidekiq.gemspec +5 -5
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +36 -13
- data/web/assets/javascripts/dashboard.js +1 -1
- data/web/assets/stylesheets/style.css +30 -6
- data/web/locales/ar.yml +1 -0
- data/web/locales/cs.yml +1 -0
- data/web/locales/da.yml +1 -0
- data/web/locales/de.yml +1 -0
- data/web/locales/el.yml +1 -0
- data/web/locales/en.yml +1 -0
- data/web/locales/es.yml +1 -0
- data/web/locales/fa.yml +1 -0
- data/web/locales/fr.yml +2 -1
- data/web/locales/gd.yml +1 -0
- data/web/locales/he.yml +1 -0
- data/web/locales/hi.yml +1 -0
- data/web/locales/it.yml +8 -0
- data/web/locales/ja.yml +1 -0
- data/web/locales/ko.yml +1 -0
- data/web/locales/lt.yml +1 -0
- data/web/locales/nb.yml +1 -0
- data/web/locales/nl.yml +1 -0
- data/web/locales/pl.yml +1 -0
- data/web/locales/pt-BR.yml +1 -0
- data/web/locales/pt.yml +1 -0
- data/web/locales/ru.yml +1 -0
- data/web/locales/sv.yml +1 -0
- data/web/locales/ta.yml +1 -0
- data/web/locales/tr.yml +1 -0
- data/web/locales/uk.yml +6 -5
- data/web/locales/ur.yml +1 -0
- data/web/locales/vi.yml +1 -0
- data/web/locales/zh-CN.yml +1 -0
- data/web/locales/zh-TW.yml +1 -0
- data/web/views/{_footer.erb → _footer.html.erb} +1 -1
- data/web/views/{_metrics_period_select.erb → _metrics_period_select.html.erb} +1 -1
- data/web/views/{_paging.erb → _paging.html.erb} +0 -1
- data/web/views/_poll_link.html.erb +4 -0
- data/web/views/{busy.erb → busy.html.erb} +4 -8
- data/web/views/{dashboard.erb → dashboard.html.erb} +3 -3
- data/web/views/{dead.erb → dead.html.erb} +3 -3
- data/web/views/filtering.html.erb +6 -0
- data/web/views/{layout.erb → layout.html.erb} +8 -7
- data/web/views/{metrics.erb → metrics.html.erb} +9 -8
- data/web/views/{morgue.erb → morgue.html.erb} +8 -4
- data/web/views/{queue.erb → queue.html.erb} +2 -2
- data/web/views/{queues.erb → queues.html.erb} +4 -4
- data/web/views/{retries.erb → retries.html.erb} +9 -5
- data/web/views/{retry.erb → retry.html.erb} +2 -2
- data/web/views/{scheduled.erb → scheduled.html.erb} +9 -5
- data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +2 -2
- metadata +37 -36
- data/lib/sidekiq/web/csrf_protection.rb +0 -183
- data/web/views/_poll_link.erb +0 -4
- data/web/views/filtering.erb +0 -6
- /data/web/views/{_job_info.erb → _job_info.html.erb} +0 -0
- /data/web/views/{_nav.erb → _nav.html.erb} +0 -0
- /data/web/views/{_summary.erb → _summary.html.erb} +0 -0
- /data/web/views/{metrics_for_job.erb → metrics_for_job.html.erb} +0 -0
- /data/web/views/{profiles.erb → profiles.html.erb} +0 -0
data/lib/sidekiq/scheduled.rb
CHANGED
|
@@ -72,6 +72,7 @@ module Sidekiq
|
|
|
72
72
|
include Sidekiq::Component
|
|
73
73
|
|
|
74
74
|
INITIAL_WAIT = 10
|
|
75
|
+
attr_accessor :rnd
|
|
75
76
|
|
|
76
77
|
def initialize(config)
|
|
77
78
|
@config = config
|
|
@@ -80,6 +81,7 @@ module Sidekiq
|
|
|
80
81
|
@done = false
|
|
81
82
|
@thread = nil
|
|
82
83
|
@count_calls = 0
|
|
84
|
+
@rnd = Random.new
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
# Shut down this instance, will pause until the thread is dead.
|
|
@@ -115,9 +117,9 @@ module Sidekiq
|
|
|
115
117
|
private
|
|
116
118
|
|
|
117
119
|
def wait
|
|
118
|
-
@sleeper.pop(random_poll_interval)
|
|
120
|
+
@sleeper.pop(timeout: random_poll_interval)
|
|
119
121
|
rescue Timeout::Error
|
|
120
|
-
#
|
|
122
|
+
# TODO move to exception: false
|
|
121
123
|
rescue => ex
|
|
122
124
|
# if poll_interval_average hasn't been calculated yet, we can
|
|
123
125
|
# raise an error trying to reach Redis.
|
|
@@ -151,11 +153,11 @@ module Sidekiq
|
|
|
151
153
|
|
|
152
154
|
if count < 10
|
|
153
155
|
# For small clusters, calculate a random interval that is ±50% the desired average.
|
|
154
|
-
interval * rand + interval.to_f / 2
|
|
156
|
+
interval * @rnd.rand + interval.to_f / 2
|
|
155
157
|
else
|
|
156
158
|
# With 10+ processes, we should have enough randomness to get decent polling
|
|
157
159
|
# across the entire timespan
|
|
158
|
-
interval * rand
|
|
160
|
+
interval * @rnd.rand * 2
|
|
159
161
|
end
|
|
160
162
|
end
|
|
161
163
|
|
|
@@ -223,7 +225,7 @@ module Sidekiq
|
|
|
223
225
|
total += INITIAL_WAIT unless @config[:poll_interval_average]
|
|
224
226
|
total += (5 * rand)
|
|
225
227
|
|
|
226
|
-
@sleeper.pop(total)
|
|
228
|
+
@sleeper.pop(timeout: total)
|
|
227
229
|
rescue Timeout::Error
|
|
228
230
|
ensure
|
|
229
231
|
# periodically clean out the `processes` set in Redis which can collect
|
data/lib/sidekiq/testing.rb
CHANGED
|
@@ -83,7 +83,7 @@ module Sidekiq
|
|
|
83
83
|
class EmptyQueueError < RuntimeError; end
|
|
84
84
|
|
|
85
85
|
module TestingClient
|
|
86
|
-
def atomic_push(conn, payloads)
|
|
86
|
+
private def atomic_push(conn, payloads)
|
|
87
87
|
if Sidekiq::Testing.fake?
|
|
88
88
|
payloads.each do |job|
|
|
89
89
|
job = Sidekiq.load_json(Sidekiq.dump_json(job))
|
|
@@ -7,6 +7,12 @@ module Sidekiq
|
|
|
7
7
|
class TransactionAwareClient
|
|
8
8
|
def initialize(pool: nil, config: nil)
|
|
9
9
|
@redis_client = Client.new(pool: pool, config: config)
|
|
10
|
+
@transaction_backend =
|
|
11
|
+
if ActiveRecord.version >= Gem::Version.new("7.2")
|
|
12
|
+
ActiveRecord.method(:after_all_transactions_commit)
|
|
13
|
+
else
|
|
14
|
+
AfterCommitEverywhere.method(:after_commit)
|
|
15
|
+
end
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
def batching?
|
|
@@ -20,7 +26,7 @@ module Sidekiq
|
|
|
20
26
|
# pre-allocate the JID so we can return it immediately and
|
|
21
27
|
# save it to the database as part of the transaction.
|
|
22
28
|
item["jid"] ||= SecureRandom.hex(12)
|
|
23
|
-
|
|
29
|
+
@transaction_backend.call { @redis_client.push(item) }
|
|
24
30
|
item["jid"]
|
|
25
31
|
end
|
|
26
32
|
|
|
@@ -38,10 +44,12 @@ end
|
|
|
38
44
|
# Use `Sidekiq.transactional_push!` in your sidekiq.rb initializer
|
|
39
45
|
module Sidekiq
|
|
40
46
|
def self.transactional_push!
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
if ActiveRecord.version < Gem::Version.new("7.2")
|
|
48
|
+
begin
|
|
49
|
+
require "after_commit_everywhere"
|
|
50
|
+
rescue LoadError
|
|
51
|
+
raise %q(You need ActiveRecord >= 7.2 or to add `gem "after_commit_everywhere"` to your Gemfile to use Sidekiq's transactional client)
|
|
52
|
+
end
|
|
45
53
|
end
|
|
46
54
|
|
|
47
55
|
Sidekiq.default_job_options["client_class"] = Sidekiq::TransactionAwareClient
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
|
@@ -67,14 +67,57 @@ module Sidekiq
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def session
|
|
70
|
-
env["rack.session"]
|
|
70
|
+
env["rack.session"] || fail(<<~EOM)
|
|
71
|
+
Sidekiq::Web needs a valid Rack session. If this is a Rails app, make
|
|
72
|
+
sure you mount Sidekiq::Web *inside* your application routes:
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
Rails.application.routes.draw do
|
|
76
|
+
mount Sidekiq::Web => "/sidekiq"
|
|
77
|
+
....
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
If this is a Rails app in API mode, you need to enable sessions.
|
|
82
|
+
|
|
83
|
+
https://guides.rubyonrails.org/api_app.html#using-session-middlewares
|
|
84
|
+
|
|
85
|
+
If this is a bare Rack app, use a session middleware before Sidekiq::Web:
|
|
86
|
+
|
|
87
|
+
# first, use IRB to create a shared secret key for sessions and commit it
|
|
88
|
+
require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
|
|
89
|
+
|
|
90
|
+
# now use the secret with a session cookie middleware
|
|
91
|
+
use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
|
|
92
|
+
run Sidekiq::Web
|
|
93
|
+
|
|
94
|
+
EOM
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def logger
|
|
98
|
+
Sidekiq.logger
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# flash { "Some message to show on redirect" }
|
|
102
|
+
def flash
|
|
103
|
+
msg = yield
|
|
104
|
+
logger.info msg
|
|
105
|
+
session[:skq_flash] = msg
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def flash?
|
|
109
|
+
session&.[](:skq_flash)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def get_flash
|
|
113
|
+
@flash ||= session.delete(:skq_flash)
|
|
71
114
|
end
|
|
72
115
|
|
|
73
116
|
def erb(content, options = {})
|
|
74
117
|
if content.is_a? Symbol
|
|
75
118
|
unless respond_to?(:"_erb_#{content}")
|
|
76
119
|
views = options[:views] || Web.views
|
|
77
|
-
filename = "#{views}/#{content}.erb"
|
|
120
|
+
filename = "#{views}/#{content}.html.erb"
|
|
78
121
|
src = ERB.new(File.read(filename)).src
|
|
79
122
|
|
|
80
123
|
# Need to use lineno less by 1 because erb generates a
|
|
@@ -318,6 +318,16 @@ module Sidekiq
|
|
|
318
318
|
redirect_with_query("#{root_path}scheduled")
|
|
319
319
|
end
|
|
320
320
|
|
|
321
|
+
post "/scheduled/all/delete" do
|
|
322
|
+
Sidekiq::ScheduledSet.new.clear
|
|
323
|
+
redirect "#{root_path}scheduled"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
post "/scheduled/all/add_to_queue" do
|
|
327
|
+
Sidekiq::ScheduledSet.new.each(&:add_to_queue)
|
|
328
|
+
redirect "#{root_path}scheduled"
|
|
329
|
+
end
|
|
330
|
+
|
|
321
331
|
get "/dashboard/stats" do
|
|
322
332
|
redirect "#{root_path}stats"
|
|
323
333
|
end
|
|
@@ -325,6 +335,8 @@ module Sidekiq
|
|
|
325
335
|
get "/stats" do
|
|
326
336
|
sidekiq_stats = Sidekiq::Stats.new
|
|
327
337
|
redis_stats = redis_info.slice(*REDIS_KEYS)
|
|
338
|
+
redis_stats["store_name"] = store_name
|
|
339
|
+
redis_stats["store_version"] = store_version
|
|
328
340
|
json(
|
|
329
341
|
sidekiq: {
|
|
330
342
|
processed: sidekiq_stats.processed,
|
|
@@ -360,10 +372,16 @@ module Sidekiq
|
|
|
360
372
|
unless sid
|
|
361
373
|
require "net/http"
|
|
362
374
|
data = Sidekiq.redis { |c| c.hget(key, "data") }
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
375
|
+
|
|
376
|
+
store_uri = URI(store)
|
|
377
|
+
http = Net::HTTP.new(store_uri.host, store_uri.port)
|
|
378
|
+
http.use_ssl = store_uri.scheme == "https"
|
|
379
|
+
request = Net::HTTP::Post.new(store_uri.request_uri)
|
|
380
|
+
request.body = data
|
|
381
|
+
request["Accept"] = "application/vnd.firefox-profiler+json;version=1.0"
|
|
382
|
+
request["User-Agent"] = "Sidekiq #{Sidekiq::VERSION} job profiler"
|
|
383
|
+
|
|
384
|
+
resp = http.request(request)
|
|
367
385
|
# https://raw.githubusercontent.com/firefox-devtools/profiler-server/master/tools/decode_jwt_payload.py
|
|
368
386
|
rawjson = resp.body.split(".")[1].unpack1("m")
|
|
369
387
|
sid = Sidekiq.load_json(rawjson)["profileToken"]
|
data/lib/sidekiq/web/config.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sidekiq/web/csrf_protection"
|
|
4
|
-
|
|
5
3
|
module Sidekiq
|
|
6
4
|
class Web
|
|
7
5
|
##
|
|
@@ -51,13 +49,15 @@ module Sidekiq
|
|
|
51
49
|
|
|
52
50
|
# Adds the "Back to App" link in the header
|
|
53
51
|
attr_accessor :app_url
|
|
52
|
+
attr_accessor :assets_path
|
|
54
53
|
|
|
55
54
|
def initialize
|
|
56
55
|
@options = OPTIONS.dup
|
|
57
56
|
@locales = LOCALES
|
|
58
57
|
@views = VIEWS
|
|
58
|
+
@assets_path = ASSETS
|
|
59
59
|
@tabs = DEFAULT_TABS.dup
|
|
60
|
-
@middlewares = [
|
|
60
|
+
@middlewares = []
|
|
61
61
|
@custom_job_info_rows = []
|
|
62
62
|
end
|
|
63
63
|
|
data/lib/sidekiq/web/helpers.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "yaml"
|
|
5
|
-
require "cgi"
|
|
5
|
+
require "cgi/escape"
|
|
6
6
|
|
|
7
7
|
module Sidekiq
|
|
8
8
|
# These methods are available to pages within the Web UI and UI extensions.
|
|
@@ -122,8 +122,8 @@ module Sidekiq
|
|
|
122
122
|
resultset
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
def filtering(which)
|
|
126
|
-
erb(:filtering, locals: {which:
|
|
125
|
+
def filtering(which, placeholder_key: "AnyJobContent", label_key: "Filter")
|
|
126
|
+
erb(:filtering, locals: {which:, placeholder_key:, label_key:})
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
def filter_link(jid, within = "retries")
|
|
@@ -165,7 +165,10 @@ module Sidekiq
|
|
|
165
165
|
text_direction == "rtl"
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
# See https://www.
|
|
168
|
+
# See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.4
|
|
169
|
+
# Returns an array of language tags ordered by their quality value
|
|
170
|
+
#
|
|
171
|
+
# Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
|
|
169
172
|
def user_preferred_languages
|
|
170
173
|
languages = env["HTTP_ACCEPT_LANGUAGE"]
|
|
171
174
|
languages.to_s.gsub(/\s+/, "").split(",").map { |language|
|
|
@@ -180,28 +183,30 @@ module Sidekiq
|
|
|
180
183
|
|
|
181
184
|
# Given an Accept-Language header like "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2"
|
|
182
185
|
# this method will try to best match the available locales to the user's preferred languages.
|
|
183
|
-
#
|
|
184
|
-
# Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
|
|
185
186
|
def locale
|
|
186
187
|
# session[:locale] is set via the locale selector from the footer
|
|
187
188
|
@locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
|
|
188
189
|
l
|
|
189
190
|
else
|
|
191
|
+
matched_locale = nil
|
|
192
|
+
# Attempt to find a case-insensitive exact match first
|
|
193
|
+
user_preferred_languages.each do |preferred|
|
|
194
|
+
# We only care about the language and primary subtag
|
|
195
|
+
# "en-GB-oxendict" becomes "en-GB"
|
|
196
|
+
language_tag = preferred.split("-")[0..1].join("-")
|
|
197
|
+
matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(language_tag) }
|
|
198
|
+
break if matched_locale
|
|
199
|
+
end
|
|
190
200
|
|
|
191
|
-
|
|
192
|
-
matched_locale = user_preferred_languages.find { |preferred|
|
|
193
|
-
available_locales.include?(preferred) if preferred.length == 5
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
matched_locale ||= user_preferred_languages.map { |preferred|
|
|
197
|
-
preferred_language = preferred.split("-", 2).first
|
|
198
|
-
|
|
199
|
-
lang_group = available_locales.select { |available|
|
|
200
|
-
preferred_language == available.split("-", 2).first
|
|
201
|
-
}
|
|
201
|
+
return matched_locale if matched_locale
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
# Find the first base language match
|
|
204
|
+
# "en-US,es-MX;q=0.9" matches "en"
|
|
205
|
+
user_preferred_languages.each do |preferred|
|
|
206
|
+
base_language = preferred.split("-", 2).first
|
|
207
|
+
matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(base_language) }
|
|
208
|
+
break if matched_locale
|
|
209
|
+
end
|
|
205
210
|
|
|
206
211
|
matched_locale || "en"
|
|
207
212
|
end
|
|
@@ -251,14 +256,6 @@ module Sidekiq
|
|
|
251
256
|
end
|
|
252
257
|
end
|
|
253
258
|
|
|
254
|
-
def busy_weights(capsule_weights)
|
|
255
|
-
# backwards compat with 7.0.0, remove in 7.1
|
|
256
|
-
cw = [capsule_weights].flatten
|
|
257
|
-
cw.map { |hash|
|
|
258
|
-
hash.map { |name, weight| (weight > 0) ? +name << ": " << weight.to_s : name }.join(", ")
|
|
259
|
-
}.join("; ")
|
|
260
|
-
end
|
|
261
|
-
|
|
262
259
|
def stats
|
|
263
260
|
@stats ||= Sidekiq::Stats.new
|
|
264
261
|
end
|
|
@@ -332,7 +329,7 @@ module Sidekiq
|
|
|
332
329
|
end
|
|
333
330
|
|
|
334
331
|
def csrf_tag
|
|
335
|
-
"
|
|
332
|
+
""
|
|
336
333
|
end
|
|
337
334
|
|
|
338
335
|
def csp_nonce
|
|
@@ -372,7 +369,7 @@ module Sidekiq
|
|
|
372
369
|
elsif rss_kb < 10_000_000
|
|
373
370
|
"#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
|
|
374
371
|
else
|
|
375
|
-
"#{number_with_delimiter(
|
|
372
|
+
"#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
|
|
376
373
|
end
|
|
377
374
|
end
|
|
378
375
|
|
data/lib/sidekiq/web.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Sidekiq
|
|
|
13
13
|
ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web")
|
|
14
14
|
VIEWS = "#{ROOT}/views"
|
|
15
15
|
LOCALES = ["#{ROOT}/locales"]
|
|
16
|
-
LAYOUT = "#{VIEWS}/layout.erb"
|
|
16
|
+
LAYOUT = "#{VIEWS}/layout.html.erb"
|
|
17
17
|
ASSETS = "#{ROOT}/assets"
|
|
18
18
|
|
|
19
19
|
DEFAULT_TABS = {
|
|
@@ -42,6 +42,12 @@ module Sidekiq
|
|
|
42
42
|
@@config.app_url = url
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def assets_path=(path)
|
|
46
|
+
@@config.assets_path = path
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def assets_path = @@config.assets_path
|
|
50
|
+
|
|
45
51
|
def tabs = @@config.tabs
|
|
46
52
|
|
|
47
53
|
def locales = @@config.locales
|
|
@@ -87,13 +93,27 @@ module Sidekiq
|
|
|
87
93
|
env[:web_config] = Sidekiq::Web.configure
|
|
88
94
|
env[:csp_nonce] = SecureRandom.hex(8)
|
|
89
95
|
env[:redis_pool] = self.class.redis_pool
|
|
90
|
-
app.call(env)
|
|
96
|
+
safe_request?(env) ? app.call(env) : deny(env)
|
|
91
97
|
end
|
|
92
98
|
|
|
93
99
|
def app
|
|
94
100
|
@app ||= build(@@config)
|
|
95
101
|
end
|
|
96
102
|
|
|
103
|
+
def safe_methods?(env)
|
|
104
|
+
%w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def safe_request?(env)
|
|
108
|
+
return true if safe_methods?(env)
|
|
109
|
+
env["HTTP_SEC_FETCH_SITE"] == "same-origin"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def deny(env)
|
|
113
|
+
Sidekiq.logger.warn "attack prevented by #{self.class}"
|
|
114
|
+
[403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
|
|
115
|
+
end
|
|
116
|
+
|
|
97
117
|
private
|
|
98
118
|
|
|
99
119
|
def build(cfg)
|
|
@@ -105,7 +125,7 @@ module Sidekiq
|
|
|
105
125
|
|
|
106
126
|
::Rack::Builder.new do
|
|
107
127
|
use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
|
|
108
|
-
root:
|
|
128
|
+
root: cfg.assets_path,
|
|
109
129
|
cascade: true,
|
|
110
130
|
header_rules: rules
|
|
111
131
|
m.each { |middleware, block| use(*middleware, &block) }
|
data/lib/sidekiq.rb
CHANGED
|
@@ -29,6 +29,7 @@ end
|
|
|
29
29
|
|
|
30
30
|
require "sidekiq/config"
|
|
31
31
|
require "sidekiq/logger"
|
|
32
|
+
require "sidekiq/loader"
|
|
32
33
|
require "sidekiq/client"
|
|
33
34
|
require "sidekiq/transaction_aware_client"
|
|
34
35
|
require "sidekiq/job"
|
|
@@ -94,6 +95,10 @@ module Sidekiq
|
|
|
94
95
|
default_configuration.logger
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
def self.loader
|
|
99
|
+
@loader ||= Loader.new
|
|
100
|
+
end
|
|
101
|
+
|
|
97
102
|
def self.configure_server(&block)
|
|
98
103
|
(@config_blocks ||= []) << block
|
|
99
104
|
yield default_configuration if server?
|
data/sidekiq.gemspec
CHANGED
|
@@ -23,9 +23,9 @@ Gem::Specification.new do |gem|
|
|
|
23
23
|
"rubygems_mfa_required" => "true"
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
gem.add_dependency "redis-client", ">= 0.
|
|
27
|
-
gem.add_dependency "connection_pool", ">=
|
|
28
|
-
gem.add_dependency "rack", ">= 3.
|
|
29
|
-
gem.add_dependency "json", ">= 2.
|
|
30
|
-
gem.add_dependency "logger", ">= 1.
|
|
26
|
+
gem.add_dependency "redis-client", ">= 0.26.0"
|
|
27
|
+
gem.add_dependency "connection_pool", ">= 3.0.0"
|
|
28
|
+
gem.add_dependency "rack", ">= 3.2.0"
|
|
29
|
+
gem.add_dependency "json", ">= 2.16.0"
|
|
30
|
+
gem.add_dependency "logger", ">= 1.7.0"
|
|
31
31
|
end
|
data/web/assets/images/logo.png
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -18,19 +18,11 @@ function addListeners() {
|
|
|
18
18
|
})
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
document.querySelectorAll("input[data-confirm]").forEach(node => {
|
|
22
|
-
node.addEventListener("click", event => {
|
|
23
|
-
if (!window.confirm(node.getAttribute("data-confirm"))) {
|
|
24
|
-
event.preventDefault();
|
|
25
|
-
event.stopPropagation();
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
|
|
30
21
|
document.querySelectorAll("[data-toggle]").forEach(node => {
|
|
31
22
|
node.addEventListener("click", addDataToggleListeners)
|
|
32
23
|
})
|
|
33
24
|
|
|
25
|
+
initializeBulkToggle();
|
|
34
26
|
addShiftClickListeners();
|
|
35
27
|
updateFuzzyTimes();
|
|
36
28
|
updateNumbers();
|
|
@@ -67,11 +59,26 @@ function addPollingListeners(_event) {
|
|
|
67
59
|
|
|
68
60
|
function addDataToggleListeners(event) {
|
|
69
61
|
var source = event.target || event.srcElement;
|
|
70
|
-
var targName = source.
|
|
62
|
+
var targName = source.dataset.toggle;
|
|
71
63
|
var full = document.getElementById(targName);
|
|
72
64
|
full.classList.toggle("is-open");
|
|
73
65
|
}
|
|
74
66
|
|
|
67
|
+
function toggleBulkButtons() {
|
|
68
|
+
const checkboxes = document.querySelectorAll('.select-item-checkbox, .check-all-items');
|
|
69
|
+
const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
|
|
70
|
+
const buttons = document.querySelectorAll('.bulk-action-buttons');
|
|
71
|
+
buttons.forEach(btn => {
|
|
72
|
+
btn.style.display = anyChecked ? 'none' : 'block';
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function initializeBulkToggle(){
|
|
77
|
+
document.querySelectorAll('.check-all-items, .select-item-checkbox').forEach(cb => {
|
|
78
|
+
cb.addEventListener('change', toggleBulkButtons);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
function addShiftClickListeners() {
|
|
76
83
|
let checkboxes = Array.from(document.querySelectorAll(".shift_clickable"));
|
|
77
84
|
let lastChecked = null;
|
|
@@ -90,7 +97,7 @@ function addShiftClickListeners() {
|
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
function updateFuzzyTimes() {
|
|
93
|
-
var locale = document.body.
|
|
100
|
+
var locale = document.body.dataset.locale;
|
|
94
101
|
var parts = locale.split('-');
|
|
95
102
|
if (typeof parts[1] !== 'undefined') {
|
|
96
103
|
parts[1] = parts[1].toUpperCase();
|
|
@@ -105,7 +112,7 @@ function updateFuzzyTimes() {
|
|
|
105
112
|
function updateNumbers() {
|
|
106
113
|
document.querySelectorAll("[data-nwp]").forEach(node => {
|
|
107
114
|
let number = parseFloat(node.textContent);
|
|
108
|
-
let precision = parseInt(node.dataset
|
|
115
|
+
let precision = parseInt(node.dataset.nwp || 0);
|
|
109
116
|
if (typeof number === "number") {
|
|
110
117
|
let formatted = number.toLocaleString(undefined, {
|
|
111
118
|
minimumFractionDigits: precision,
|
|
@@ -178,4 +185,20 @@ function updateLocale(event) {
|
|
|
178
185
|
|
|
179
186
|
function updateProgressBars() {
|
|
180
187
|
document.querySelectorAll('.progress-bar').forEach(bar => { bar.style.width = bar.dataset.width + "%"})
|
|
181
|
-
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleConfirmDialog (event) {
|
|
191
|
+
const target = event.target
|
|
192
|
+
|
|
193
|
+
if (target.localName !== "input" && target.localName !== "button" ) { return }
|
|
194
|
+
const confirmMessage = target.dataset.confirm
|
|
195
|
+
|
|
196
|
+
if (confirmMessage === undefined) { return }
|
|
197
|
+
|
|
198
|
+
if (!window.confirm(confirmMessage)) {
|
|
199
|
+
event.preventDefault()
|
|
200
|
+
event.stopPropagation()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
document.addEventListener("click", handleConfirmDialog)
|
|
@@ -11,7 +11,7 @@ var updateStatsSummary = function(data) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
var updateRedisStats = function(data) {
|
|
14
|
-
document.getElementById('redis_version').innerText = data.
|
|
14
|
+
document.getElementById('redis_version').innerText = data.store_version;
|
|
15
15
|
document.getElementById('uptime_in_days').innerText = data.uptime_in_days;
|
|
16
16
|
document.getElementById('connected_clients').innerText = data.connected_clients;
|
|
17
17
|
document.getElementById('used_memory_human').innerText = data.used_memory_human;
|
|
@@ -29,8 +29,6 @@
|
|
|
29
29
|
|
|
30
30
|
*, *::before, *::after { box-sizing: border-box; }
|
|
31
31
|
|
|
32
|
-
::selection { background: var(--color-selected); }
|
|
33
|
-
|
|
34
32
|
:focus-visible {
|
|
35
33
|
outline: 1px solid oklch(from var(--color-primary) l c h / 50%);
|
|
36
34
|
}
|
|
@@ -69,7 +67,7 @@ body {
|
|
|
69
67
|
|
|
70
68
|
.container {
|
|
71
69
|
margin: 0 auto;
|
|
72
|
-
max-width: 1440px;
|
|
70
|
+
/* max-width: 1440px; */
|
|
73
71
|
padding: var(--space-2x);
|
|
74
72
|
}
|
|
75
73
|
|
|
@@ -90,6 +88,7 @@ code {
|
|
|
90
88
|
font-family: var(--font-mono);
|
|
91
89
|
font-size: var(--font-size-small);
|
|
92
90
|
padding: var(--space-1-2);
|
|
91
|
+
word-wrap: anywhere;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
time { color: var(--color-text-light); }
|
|
@@ -437,7 +436,10 @@ article .count {
|
|
|
437
436
|
}
|
|
438
437
|
|
|
439
438
|
/* table */
|
|
440
|
-
.table_container {
|
|
439
|
+
.table_container {
|
|
440
|
+
overflow-x: auto;
|
|
441
|
+
margin-bottom: var(--space-2x);
|
|
442
|
+
}
|
|
441
443
|
|
|
442
444
|
.table_container + form,
|
|
443
445
|
.table_container + input,
|
|
@@ -446,12 +448,14 @@ canvas + .table_container {
|
|
|
446
448
|
}
|
|
447
449
|
|
|
448
450
|
.buttons-row {
|
|
449
|
-
margin-top: var(--space-3x);
|
|
450
451
|
display: flex;
|
|
451
|
-
flex-wrap: wrap;
|
|
452
452
|
gap: 8px;
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
.bulk-action-buttons.bulk-lead-button {
|
|
456
|
+
margin-inline-start: auto;
|
|
457
|
+
}
|
|
458
|
+
|
|
455
459
|
table {
|
|
456
460
|
background-color: var(--color-elevated);
|
|
457
461
|
border: 1px solid var(--color-border);
|
|
@@ -748,3 +752,23 @@ body > footer .nav {
|
|
|
748
752
|
.w-50 {
|
|
749
753
|
width: 50%;
|
|
750
754
|
}
|
|
755
|
+
|
|
756
|
+
.flash {
|
|
757
|
+
width: 100%;
|
|
758
|
+
text-align: center;
|
|
759
|
+
padding: 20px;
|
|
760
|
+
background: var(--color-success);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.args {
|
|
764
|
+
overflow-y: auto;
|
|
765
|
+
max-height: 50px;
|
|
766
|
+
word-break: break-word
|
|
767
|
+
}
|
|
768
|
+
.args-extended {
|
|
769
|
+
overflow-y: scroll;
|
|
770
|
+
max-height: 500px;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.toggle { display: none; }
|
|
774
|
+
.toggle.is-open { display: block; }
|
data/web/locales/ar.yml
CHANGED
data/web/locales/cs.yml
CHANGED
|
@@ -3,6 +3,7 @@ cs:
|
|
|
3
3
|
LanguageName: Čeština
|
|
4
4
|
Actions: Akce
|
|
5
5
|
AddToQueue: Přidat do fronty
|
|
6
|
+
AddAllToQueue: Přidat vše do fronty
|
|
6
7
|
AreYouSure: Jste si jisti?
|
|
7
8
|
AreYouSureDeleteJob: Jste si jisti, že chcete odstranit tento úkol?
|
|
8
9
|
AreYouSureDeleteQueue: Jste si jisti, že chcete odstranit frontu %{queue}?
|
data/web/locales/da.yml
CHANGED
|
@@ -3,6 +3,7 @@ da:
|
|
|
3
3
|
LanguageName: Dansk
|
|
4
4
|
Actions: Handlinger
|
|
5
5
|
AddToQueue: Tilføj til kø
|
|
6
|
+
AddAllToQueue: Tilføj alle til kø
|
|
6
7
|
AreYouSure: Er du sikker?
|
|
7
8
|
AreYouSureDeleteJob: Er du sikker på at du vil slette dette job?
|
|
8
9
|
AreYouSureDeleteQueue: Er du sikker på at du vil slette %{queue} køen?
|
data/web/locales/de.yml
CHANGED
|
@@ -3,6 +3,7 @@ de:
|
|
|
3
3
|
LanguageName: Deutsch
|
|
4
4
|
Actions: Aktionen
|
|
5
5
|
AddToQueue: In Warteschlange einreihen
|
|
6
|
+
AddAllToQueue: Alle in Warteschlange einreihen
|
|
6
7
|
AreYouSure: Bist du sicher?
|
|
7
8
|
AreYouSureDeleteJob: Möchtest du diesen Job wirklich löschen?
|
|
8
9
|
AreYouSureDeleteQueue: Möchtest du %{queue} wirklich löschen?
|
data/web/locales/el.yml
CHANGED
|
@@ -26,6 +26,7 @@ el: # <---- change this to your locale code
|
|
|
26
26
|
ShowAll: Εμφάνιση Όλων
|
|
27
27
|
Enqueued: Μπήκαν στην στοίβα
|
|
28
28
|
AddToQueue: Προσθήκη στην στοίβα
|
|
29
|
+
AddAllToQueue: Προσθήκη όλων στην στοίβα
|
|
29
30
|
AreYouSureDeleteJob: Θέλετε να διαγράψετε αυτή την εργασία;
|
|
30
31
|
AreYouSureDeleteQueue: Θέλετε να διαγράψετε την στοίβα %{queue}; Αυτό θα διαγράψει όλες τις εργασίες εντός της στοίβας, θα εμφανιστεί ξανά εάν προωθήσετε περισσότερες εργασίες σε αυτήν στο μέλλον.
|
|
31
32
|
Queues: Στοίβες
|