sidekiq 6.2.2 → 8.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Changes.md +726 -11
- data/LICENSE.txt +9 -0
- data/README.md +70 -39
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +4 -9
- data/bin/sidekiqload +214 -115
- data/bin/sidekiqmon +4 -1
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/lib/generators/sidekiq/job_generator.rb +71 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
- data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
- data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
- data/lib/sidekiq/api.rb +729 -264
- data/lib/sidekiq/capsule.rb +135 -0
- data/lib/sidekiq/cli.rb +124 -100
- data/lib/sidekiq/client.rb +153 -106
- data/lib/sidekiq/component.rb +132 -0
- data/lib/sidekiq/config.rb +320 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +64 -0
- data/lib/sidekiq/fetch.rb +27 -26
- data/lib/sidekiq/iterable_job.rb +56 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -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 +322 -0
- data/lib/sidekiq/job.rb +397 -5
- data/lib/sidekiq/job_logger.rb +23 -32
- data/lib/sidekiq/job_retry.rb +141 -68
- data/lib/sidekiq/job_util.rb +113 -0
- data/lib/sidekiq/launcher.rb +122 -98
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +27 -106
- data/lib/sidekiq/manager.rb +41 -43
- data/lib/sidekiq/metrics/query.rb +184 -0
- data/lib/sidekiq/metrics/shared.rb +109 -0
- data/lib/sidekiq/metrics/tracking.rb +153 -0
- data/lib/sidekiq/middleware/chain.rb +96 -51
- data/lib/sidekiq/middleware/current_attributes.rb +120 -0
- data/lib/sidekiq/middleware/i18n.rb +8 -4
- data/lib/sidekiq/middleware/modules.rb +23 -0
- data/lib/sidekiq/monitor.rb +16 -6
- data/lib/sidekiq/paginator.rb +37 -10
- data/lib/sidekiq/processor.rb +105 -87
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +49 -36
- data/lib/sidekiq/redis_client_adapter.rb +117 -0
- data/lib/sidekiq/redis_connection.rb +55 -86
- data/lib/sidekiq/ring_buffer.rb +32 -0
- data/lib/sidekiq/scheduled.rb +106 -50
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -342
- data/lib/sidekiq/transaction_aware_client.rb +59 -0
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
- data/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +382 -0
- data/lib/sidekiq/version.rb +6 -1
- data/lib/sidekiq/web/action.rb +149 -64
- data/lib/sidekiq/web/application.rb +376 -268
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +213 -87
- data/lib/sidekiq/web/router.rb +61 -74
- data/lib/sidekiq/web.rb +71 -100
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +95 -196
- data/sidekiq.gemspec +14 -11
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +171 -57
- data/web/assets/javascripts/base-charts.js +120 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +194 -0
- data/web/assets/javascripts/dashboard.js +41 -274
- data/web/assets/javascripts/metrics.js +280 -0
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +72 -70
- data/web/locales/cs.yml +64 -62
- data/web/locales/da.yml +62 -53
- data/web/locales/de.yml +67 -65
- data/web/locales/el.yml +45 -24
- data/web/locales/en.yml +93 -69
- data/web/locales/es.yml +91 -68
- data/web/locales/fa.yml +67 -65
- data/web/locales/fr.yml +82 -67
- data/web/locales/gd.yml +110 -0
- data/web/locales/he.yml +67 -64
- data/web/locales/hi.yml +61 -59
- data/web/locales/it.yml +94 -54
- data/web/locales/ja.yml +74 -68
- data/web/locales/ko.yml +54 -52
- data/web/locales/lt.yml +68 -66
- data/web/locales/nb.yml +63 -61
- data/web/locales/nl.yml +54 -52
- data/web/locales/pl.yml +47 -45
- data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
- data/web/locales/pt.yml +53 -51
- data/web/locales/ru.yml +69 -66
- data/web/locales/sv.yml +55 -53
- data/web/locales/ta.yml +62 -60
- data/web/locales/tr.yml +102 -0
- data/web/locales/uk.yml +87 -61
- data/web/locales/ur.yml +66 -64
- data/web/locales/vi.yml +69 -67
- data/web/locales/zh-CN.yml +107 -0
- data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
- data/web/views/_footer.html.erb +32 -0
- data/web/views/_job_info.html.erb +115 -0
- data/web/views/_metrics_period_select.html.erb +15 -0
- data/web/views/_nav.html.erb +45 -0
- data/web/views/_paging.html.erb +26 -0
- data/web/views/_poll_link.html.erb +4 -0
- data/web/views/_summary.html.erb +40 -0
- data/web/views/busy.html.erb +151 -0
- data/web/views/dashboard.html.erb +104 -0
- data/web/views/dead.html.erb +38 -0
- data/web/views/filtering.html.erb +6 -0
- data/web/views/layout.html.erb +26 -0
- data/web/views/metrics.html.erb +85 -0
- data/web/views/metrics_for_job.html.erb +58 -0
- data/web/views/morgue.html.erb +69 -0
- data/web/views/profiles.html.erb +43 -0
- data/web/views/queue.html.erb +57 -0
- data/web/views/queues.html.erb +46 -0
- data/web/views/retries.html.erb +77 -0
- data/web/views/retry.html.erb +39 -0
- data/web/views/scheduled.html.erb +64 -0
- data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
- metadata +130 -61
- data/LICENSE +0 -9
- data/lib/generators/sidekiq/worker_generator.rb +0 -57
- data/lib/sidekiq/delay.rb +0 -41
- data/lib/sidekiq/exception_handler.rb +0 -27
- data/lib/sidekiq/extensions/action_mailer.rb +0 -48
- data/lib/sidekiq/extensions/active_record.rb +0 -43
- data/lib/sidekiq/extensions/class_methods.rb +0 -43
- data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
- data/lib/sidekiq/util.rb +0 -95
- data/lib/sidekiq/web/csrf_protection.rb +0 -180
- data/lib/sidekiq/worker.rb +0 -244
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -246
- data/web/assets/stylesheets/application.css +0 -1053
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/locales/zh-cn.yml +0 -68
- data/web/views/_footer.erb +0 -20
- data/web/views/_job_info.erb +0 -89
- data/web/views/_nav.erb +0 -52
- data/web/views/_paging.erb +0 -23
- data/web/views/_poll_link.erb +0 -7
- data/web/views/_status.erb +0 -4
- data/web/views/_summary.erb +0 -40
- data/web/views/busy.erb +0 -132
- data/web/views/dashboard.erb +0 -83
- data/web/views/dead.erb +0 -34
- data/web/views/layout.erb +0 -42
- data/web/views/morgue.erb +0 -78
- data/web/views/queue.erb +0 -55
- data/web/views/queues.erb +0 -38
- data/web/views/retries.erb +0 -83
- data/web/views/retry.erb +0 -34
- data/web/views/scheduled.erb +0 -57
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "sidekiq/client"
|
|
5
|
+
|
|
6
|
+
module Sidekiq
|
|
7
|
+
class TransactionAwareClient
|
|
8
|
+
def initialize(pool: nil, config: nil)
|
|
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
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def batching?
|
|
19
|
+
Thread.current[:sidekiq_batch]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def push(item)
|
|
23
|
+
# 6160 we can't support both Sidekiq::Batch and transactions.
|
|
24
|
+
return @redis_client.push(item) if batching?
|
|
25
|
+
|
|
26
|
+
# pre-allocate the JID so we can return it immediately and
|
|
27
|
+
# save it to the database as part of the transaction.
|
|
28
|
+
item["jid"] ||= SecureRandom.hex(12)
|
|
29
|
+
@transaction_backend.call { @redis_client.push(item) }
|
|
30
|
+
item["jid"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# We don't provide transactionality for push_bulk because we don't want
|
|
35
|
+
# to hold potentially hundreds of thousands of job records in memory due to
|
|
36
|
+
# a long running enqueue process.
|
|
37
|
+
def push_bulk(items)
|
|
38
|
+
@redis_client.push_bulk(items)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Use `Sidekiq.transactional_push!` in your sidekiq.rb initializer
|
|
45
|
+
module Sidekiq
|
|
46
|
+
def self.transactional_push!
|
|
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
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Sidekiq.default_job_options["client_class"] = Sidekiq::TransactionAwareClient
|
|
56
|
+
Sidekiq::JobUtil::TRANSIENT_ATTRIBUTES << "client_class"
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
class TUI
|
|
3
|
+
module Controls
|
|
4
|
+
# Defines data for input handling and for displaying controls.
|
|
5
|
+
# :code is the key code for input handling.
|
|
6
|
+
# :display and :description are shown in the controls area, with different
|
|
7
|
+
# styling between them. If :display is omitted, :code is displayed instead.
|
|
8
|
+
# :action is a lambda to execute when the control is triggered.
|
|
9
|
+
# :refresh means the action requires immediate refreshing of data
|
|
10
|
+
#
|
|
11
|
+
# Conventions: dangerous/irreversible actions should use UPPERCASE codes.
|
|
12
|
+
# The Shift button means "I'm sure".
|
|
13
|
+
GLOBAL = [
|
|
14
|
+
{code: "?", display: "?", description: "Help", action: ->(tui, tab) { tui.show_help }},
|
|
15
|
+
{code: "left", display: "←/→", description: "Select Tab", action: ->(tui, tab) { tui.navigate(:left) }, refresh: true},
|
|
16
|
+
{code: "right", action: ->(tui, tab) { tui.navigate(:right) }, refresh: true},
|
|
17
|
+
{code: "q", description: "Quit", action: ->(tui, tab) { :quit }},
|
|
18
|
+
{code: "c", modifiers: ["ctrl"], action: ->(tui, tab) { :quit }}
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
SHARED = {
|
|
22
|
+
pageable: [
|
|
23
|
+
{code: "h", display: "h/l", description: "Prev/Next Page",
|
|
24
|
+
action: ->(tui, tab) { tab.prev_page }, refresh: true},
|
|
25
|
+
{code: "l", action: ->(tui, tab) { tab.next_page }, refresh: true}
|
|
26
|
+
],
|
|
27
|
+
selectable: [
|
|
28
|
+
{code: "k", display: "j/k", description: "Prev/Next Row",
|
|
29
|
+
action: ->(tui, tab) { tab.navigate_row(:up) }},
|
|
30
|
+
{code: "j", action: ->(tui, tab) { tab.navigate_row(:down) }},
|
|
31
|
+
{code: "x", description: "Select", action: ->(tui, tab) { tab.toggle_select }},
|
|
32
|
+
{code: "A", modifiers: ["shift"], display: "A", description: "Select All",
|
|
33
|
+
action: ->(tui, tab) { tab.toggle_select(:all) }}
|
|
34
|
+
],
|
|
35
|
+
filterable: [
|
|
36
|
+
{code: "/", display: "/", description: "Filter", action: ->(tui, tab) { tab.start_filtering }},
|
|
37
|
+
{code: "backspace", action: ->(tui, tab) { tab.remove_last_char_from_filter }, refresh: true},
|
|
38
|
+
{code: "enter", action: ->(tui, tab) { tab.stop_filtering }, refresh: true},
|
|
39
|
+
{code: "esc", action: ->(tui, tab) { tab.stop_and_clear_filtering }, refresh: true}
|
|
40
|
+
]
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Returns an array of symbols for functionality which this tab implements
|
|
44
|
+
def features
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def controls
|
|
49
|
+
GLOBAL + SHARED.slice(*features).values.flatten
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
class TUI
|
|
3
|
+
module Filtering
|
|
4
|
+
def filtering?
|
|
5
|
+
@data[:filtering]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def current_filter
|
|
9
|
+
@data[:filter]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start_filtering
|
|
13
|
+
@data[:filtering] = true
|
|
14
|
+
@data[:filter] = ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stop_filtering
|
|
18
|
+
return unless @data[:filtering]
|
|
19
|
+
|
|
20
|
+
@data[:filtering] = false
|
|
21
|
+
@data[:selected] = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop_and_clear_filtering
|
|
25
|
+
return unless @data[:filtering]
|
|
26
|
+
|
|
27
|
+
@data[:filtering] = false
|
|
28
|
+
@data[:filter] = nil
|
|
29
|
+
@data[:selected] = []
|
|
30
|
+
on_filter_change
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def remove_last_char_from_filter
|
|
34
|
+
return unless @data[:filtering]
|
|
35
|
+
|
|
36
|
+
@data[:filter] = @data[:filter].empty? ? "" : @data[:filter][0..-2]
|
|
37
|
+
on_filter_change
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def append_to_filter(string)
|
|
41
|
+
return unless @data[:filtering]
|
|
42
|
+
|
|
43
|
+
@data[:filter] += string
|
|
44
|
+
@data[:selected] = []
|
|
45
|
+
on_filter_change
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_filter_change
|
|
49
|
+
# callback for subclasses
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
class TUI
|
|
3
|
+
class BaseTab
|
|
4
|
+
include Controls
|
|
5
|
+
|
|
6
|
+
attr_reader :name
|
|
7
|
+
attr_reader :data
|
|
8
|
+
|
|
9
|
+
def initialize(parent)
|
|
10
|
+
@parent = parent
|
|
11
|
+
@name = self.class.name.split("::").last
|
|
12
|
+
reset_data
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def t(*)
|
|
16
|
+
@parent.t(*)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset_data
|
|
20
|
+
@data = {selected: [], selected_row_index: 0}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def error
|
|
24
|
+
@data[:error]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def error=(e)
|
|
28
|
+
@data[:error] = e
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def selected?(entry)
|
|
32
|
+
@data[:selected].index(entry.id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def filtering?
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def each_selection(unselect: true, &)
|
|
40
|
+
sel = @data[:selected]
|
|
41
|
+
finished = []
|
|
42
|
+
if !sel.empty?
|
|
43
|
+
sel.each do |id|
|
|
44
|
+
yield id
|
|
45
|
+
# When processing multiple items in bulk, we want to unselect
|
|
46
|
+
# each row if its operation succeeds so our UI will not
|
|
47
|
+
# re-process rows 1-3 if row 4 fails.
|
|
48
|
+
finished << id
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
ids = @data.dig(:table, :row_ids)
|
|
52
|
+
return if !ids || ids.empty?
|
|
53
|
+
yield ids[@data[:selected_row_index]]
|
|
54
|
+
end
|
|
55
|
+
ensure
|
|
56
|
+
@data[:selected] = sel - finished if unselect
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Navigate the row selection up or down in the current tab's table.
|
|
60
|
+
# @param direction [Symbol] :up or :down
|
|
61
|
+
def navigate_row(direction)
|
|
62
|
+
ids = @data.dig(:table, :row_ids)
|
|
63
|
+
return if !ids || ids.empty?
|
|
64
|
+
|
|
65
|
+
index_change = (direction == :down) ? 1 : -1
|
|
66
|
+
@data[:selected_row_index] = (@data[:selected_row_index] + index_change) % ids.count
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def prev_page
|
|
70
|
+
opts = @data.dig(:table, :pager)
|
|
71
|
+
return unless opts
|
|
72
|
+
return if opts.page < 2
|
|
73
|
+
|
|
74
|
+
@data[:table][:pager] = Sidekiq::TUI::PageOptions.new(opts.page - 1, opts.size)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def next_page
|
|
78
|
+
np = @data.dig(:table, :next_page)
|
|
79
|
+
return unless np
|
|
80
|
+
opts = @data.dig(:table, :pager)
|
|
81
|
+
return unless opts
|
|
82
|
+
|
|
83
|
+
@data[:table][:pager] = Sidekiq::TUI::PageOptions.new(np, opts.size)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def toggle_select(which = :current)
|
|
87
|
+
sel = @data[:selected]
|
|
88
|
+
# log(which, sel)
|
|
89
|
+
if which == :current
|
|
90
|
+
x = @data[:table][:row_ids][@data[:selected_row_index]]
|
|
91
|
+
if sel.index(x)
|
|
92
|
+
# already checked, uncheck it
|
|
93
|
+
sel.delete(x)
|
|
94
|
+
else
|
|
95
|
+
sel << x
|
|
96
|
+
end
|
|
97
|
+
elsif sel.empty?
|
|
98
|
+
@data[:selected] = @data[:table][:row_ids]
|
|
99
|
+
else
|
|
100
|
+
sel.clear
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def refresh_data_for_stats
|
|
105
|
+
stats = Sidekiq::Stats.new
|
|
106
|
+
@data[:stats] = {
|
|
107
|
+
processed: stats.processed,
|
|
108
|
+
failed: stats.failed,
|
|
109
|
+
busy: stats.workers_size,
|
|
110
|
+
enqueued: stats.enqueued,
|
|
111
|
+
retries: stats.retry_size,
|
|
112
|
+
scheduled: stats.scheduled_size,
|
|
113
|
+
dead: stats.dead_size
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_table(tui, frame, area)
|
|
118
|
+
page = @data.dig(:table, :current_page) || 1
|
|
119
|
+
rows = @data.dig(:table, :rows) || []
|
|
120
|
+
total = @data.dig(:table, :total) || 0
|
|
121
|
+
footer = ["", "Page: #{page}", "Count: #{rows.size}", "Total: #{total}"]
|
|
122
|
+
footer << "Selected: #{@data[:selected].size}" unless @data[:selected].empty?
|
|
123
|
+
|
|
124
|
+
defaults = {
|
|
125
|
+
title: "TableName",
|
|
126
|
+
footer: footer
|
|
127
|
+
}
|
|
128
|
+
if features.include?(:selectable)
|
|
129
|
+
defaults.merge!({
|
|
130
|
+
highlight_symbol: "➡️",
|
|
131
|
+
selected_row: @data[:selected_row_index],
|
|
132
|
+
row_highlight_style: tui.style(fg: :white, bg: :blue)
|
|
133
|
+
})
|
|
134
|
+
end
|
|
135
|
+
hash = defaults.merge(yield)
|
|
136
|
+
hash[:block] ||= tui.block(title: hash.delete(:title), borders: :all)
|
|
137
|
+
table = tui.table(**hash)
|
|
138
|
+
frame.render_widget(table, area)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def render_stats_section(tui, frame, area)
|
|
142
|
+
stats = @data[:stats]
|
|
143
|
+
|
|
144
|
+
keys = ["Processed", "Failed", "Busy", "Enqueued", "Retries", "Scheduled", "Dead"]
|
|
145
|
+
values = [
|
|
146
|
+
stats[:processed],
|
|
147
|
+
stats[:failed],
|
|
148
|
+
stats[:busy],
|
|
149
|
+
stats[:enqueued],
|
|
150
|
+
stats[:retries],
|
|
151
|
+
stats[:scheduled],
|
|
152
|
+
stats[:dead]
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# Format keys and values with spacing
|
|
156
|
+
keys_line = keys.map { |k| t(k).to_s.ljust(12) }.join(" ")
|
|
157
|
+
values_line = values.map { |v| number_with_delimiter(v).ljust(12) }.join(" ")
|
|
158
|
+
|
|
159
|
+
frame.render_widget(
|
|
160
|
+
tui.paragraph(
|
|
161
|
+
text: [keys_line, values_line],
|
|
162
|
+
block: tui.block(title: "Statistics", borders: [:all])
|
|
163
|
+
),
|
|
164
|
+
area
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# [thousands_separator, decimal_separator] per locale.
|
|
169
|
+
# Locales not listed here use the English default [",", "."].
|
|
170
|
+
NUMERIC_SEPARATORS = {
|
|
171
|
+
# period thousands, comma decimal
|
|
172
|
+
"da" => [".", ","], "de" => [".", ","], "el" => [".", ","],
|
|
173
|
+
"es" => [".", ","], "it" => [".", ","], "nl" => [".", ","],
|
|
174
|
+
"pt" => [".", ","], "pt-BR" => [".", ","], "tr" => [".", ","],
|
|
175
|
+
"vi" => [".", ","],
|
|
176
|
+
# space thousands, comma decimal
|
|
177
|
+
"cs" => [" ", ","], "fr" => [" ", ","], "lt" => [" ", ","],
|
|
178
|
+
"nb" => [" ", ","], "pl" => [" ", ","], "ru" => [" ", ","],
|
|
179
|
+
"sv" => [" ", ","], "uk" => [" ", ","]
|
|
180
|
+
}.freeze
|
|
181
|
+
|
|
182
|
+
def number_with_delimiter(number, options = {})
|
|
183
|
+
precision = options[:precision] || 0
|
|
184
|
+
rounded = number.round(precision)
|
|
185
|
+
thousands, decimal = NUMERIC_SEPARATORS.fetch(@parent.lang, [",", "."])
|
|
186
|
+
integer_part, decimal_part = rounded.to_s.split(".")
|
|
187
|
+
integer_with_sep = integer_part.gsub(/(\d)(?=(\d{3})+(?!\d))/, "\\1#{thousands}")
|
|
188
|
+
(precision > 0) ? "#{integer_with_sep}#{decimal}#{(decimal_part || "").ljust(precision, "0")}" : integer_with_sep
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def format_memory(rss_kb)
|
|
192
|
+
return "0" if rss_kb.nil? || rss_kb == 0
|
|
193
|
+
|
|
194
|
+
if rss_kb < 100_000
|
|
195
|
+
"#{number_with_delimiter(rss_kb)} KB"
|
|
196
|
+
elsif rss_kb < 10_000_000
|
|
197
|
+
"#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
|
|
198
|
+
else
|
|
199
|
+
"#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
|
|
3
|
+
module Sidekiq
|
|
4
|
+
class TUI
|
|
5
|
+
module Tabs
|
|
6
|
+
class Busy < BaseTab
|
|
7
|
+
def features
|
|
8
|
+
%i[selectable]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def controls
|
|
12
|
+
@controls ||= super + [
|
|
13
|
+
{code: "T", modifiers: ["shift"], description: "Terminate", action: ->(tui, tab) { tab.terminate! }},
|
|
14
|
+
{code: "Q", modifiers: ["shift"], description: "Quiet", action: ->(tui, tab) { tab.quiet! }}
|
|
15
|
+
]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def quiet!
|
|
19
|
+
each_selection do |id|
|
|
20
|
+
Sidekiq::Process.new("identity" => id).quiet!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def terminate!
|
|
25
|
+
each_selection do |id|
|
|
26
|
+
Sidekiq::Process.new("identity" => id).stop!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def refresh_data
|
|
31
|
+
refresh_data_for_stats
|
|
32
|
+
|
|
33
|
+
busy = []
|
|
34
|
+
table_row_ids = []
|
|
35
|
+
|
|
36
|
+
Sidekiq::ProcessSet.new.each do |p|
|
|
37
|
+
name = "#{p["hostname"]}:#{p["pid"]}"
|
|
38
|
+
name += " ⭐️" if p.leader?
|
|
39
|
+
name += " 🛑" if p.stopping?
|
|
40
|
+
busy << [
|
|
41
|
+
selected?(p) ? "✅" : "",
|
|
42
|
+
name,
|
|
43
|
+
Time.at(p["started_at"]).utc,
|
|
44
|
+
format_memory(p["rss"].to_i),
|
|
45
|
+
number_with_delimiter(p["concurrency"]),
|
|
46
|
+
number_with_delimiter(p["busy"])
|
|
47
|
+
]
|
|
48
|
+
table_row_ids << p.identity
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@data[:busy] = busy
|
|
52
|
+
@data[:table] = {row_ids: table_row_ids}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render(tui, frame, area)
|
|
56
|
+
chunks = tui.layout_split(
|
|
57
|
+
area,
|
|
58
|
+
direction: :vertical,
|
|
59
|
+
constraints: [
|
|
60
|
+
tui.constraint_length(4), # Stats
|
|
61
|
+
tui.constraint_length(4), # Status
|
|
62
|
+
tui.constraint_fill(1) # Graph
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
render_stats_section(tui, frame, chunks[0])
|
|
67
|
+
render_status_section(tui, frame, chunks[1])
|
|
68
|
+
render_table(tui, frame, chunks[2]) do
|
|
69
|
+
{
|
|
70
|
+
title: t("Processes"),
|
|
71
|
+
header: ["☑️", "Name", "Started", "RSS", "Threads", "Busy"].map { |x| t(x) },
|
|
72
|
+
widths: [
|
|
73
|
+
tui.constraint_length(5),
|
|
74
|
+
tui.constraint_fill(1),
|
|
75
|
+
tui.constraint_length(24),
|
|
76
|
+
tui.constraint_length(10),
|
|
77
|
+
tui.constraint_length(6),
|
|
78
|
+
tui.constraint_length(6)
|
|
79
|
+
],
|
|
80
|
+
rows: @data[:busy].map.with_index { |cells, idx|
|
|
81
|
+
tui.table_row(
|
|
82
|
+
cells:,
|
|
83
|
+
style: idx.even? ? nil : tui.style(bg: :dark_gray)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_status_section(tui, frame, area)
|
|
91
|
+
values = []
|
|
92
|
+
processes = Sidekiq::ProcessSet.new
|
|
93
|
+
workset = Sidekiq::WorkSet.new
|
|
94
|
+
ws = workset.size
|
|
95
|
+
values << (s = processes.size
|
|
96
|
+
number_with_delimiter(s))
|
|
97
|
+
values << (x = processes.total_concurrency
|
|
98
|
+
number_with_delimiter(x))
|
|
99
|
+
values << number_with_delimiter(ws)
|
|
100
|
+
values << "#{(x == 0) ? 0 : ((ws / x.to_f) * 100).round(0)}%"
|
|
101
|
+
values << format_memory(processes.total_rss)
|
|
102
|
+
|
|
103
|
+
keys = %w[Processes Threads Busy Utilization RSS]
|
|
104
|
+
keys_line = keys.map { |k| t(k).to_s.ljust(12) }.join(" ")
|
|
105
|
+
values_line = values.map { |v| v.to_s.ljust(12) }.join(" ")
|
|
106
|
+
|
|
107
|
+
frame.render_widget(
|
|
108
|
+
tui.paragraph(
|
|
109
|
+
text: [keys_line, values_line],
|
|
110
|
+
block: tui.block(title: t("Status"), borders: [:all])
|
|
111
|
+
),
|
|
112
|
+
area
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
require_relative "set_tab"
|
|
3
|
+
|
|
4
|
+
module Sidekiq
|
|
5
|
+
class TUI
|
|
6
|
+
module Tabs
|
|
7
|
+
class Dead < BaseTab
|
|
8
|
+
include SetTab
|
|
9
|
+
|
|
10
|
+
def set_class = Sidekiq::DeadSet
|
|
11
|
+
|
|
12
|
+
def refresh_data
|
|
13
|
+
refresh_data_for_stats
|
|
14
|
+
refresh_data_for_set
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
|
|
3
|
+
module Sidekiq
|
|
4
|
+
class TUI
|
|
5
|
+
module Tabs
|
|
6
|
+
class Home < BaseTab
|
|
7
|
+
def refresh_data
|
|
8
|
+
refresh_data_for_stats
|
|
9
|
+
|
|
10
|
+
stats = Sidekiq::Stats.new
|
|
11
|
+
@data[:chart] ||= {
|
|
12
|
+
previous_stats: {
|
|
13
|
+
processed: stats.processed,
|
|
14
|
+
failed: stats.failed
|
|
15
|
+
},
|
|
16
|
+
deltas: {
|
|
17
|
+
processed: Array.new(50, 0),
|
|
18
|
+
failed: Array.new(50, 0)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
processed_delta = stats.processed - @data[:chart][:previous_stats][:processed]
|
|
23
|
+
failed_delta = stats.failed - @data[:chart][:previous_stats][:failed]
|
|
24
|
+
|
|
25
|
+
@data[:chart][:deltas][:processed].shift
|
|
26
|
+
@data[:chart][:deltas][:processed].push(processed_delta)
|
|
27
|
+
@data[:chart][:deltas][:failed].shift
|
|
28
|
+
@data[:chart][:deltas][:failed].push(failed_delta)
|
|
29
|
+
|
|
30
|
+
@data[:chart][:previous_stats] = {
|
|
31
|
+
processed: stats.processed,
|
|
32
|
+
failed: stats.failed
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
redis_info = Sidekiq.default_configuration.redis_info
|
|
36
|
+
|
|
37
|
+
@data[:redis_info] = {
|
|
38
|
+
version: redis_info["redis_version"] || "N/A",
|
|
39
|
+
uptime_days: redis_info["uptime_in_days"] || "N/A",
|
|
40
|
+
connected_clients: redis_info["connected_clients"] || "N/A",
|
|
41
|
+
used_memory: redis_info["used_memory_human"] || "N/A",
|
|
42
|
+
peak_memory: redis_info["used_memory_peak_human"] || "N/A"
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render(tui, frame, area)
|
|
47
|
+
chunks = tui.layout_split(
|
|
48
|
+
area,
|
|
49
|
+
direction: :vertical,
|
|
50
|
+
constraints: [
|
|
51
|
+
tui.constraint_length(4), # Stats
|
|
52
|
+
tui.constraint_fill(1), # Graph
|
|
53
|
+
tui.constraint_length(4) # Redis
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
render_stats_section(tui, frame, chunks[0])
|
|
58
|
+
render_chart_section(tui, frame, chunks[1])
|
|
59
|
+
render_redis_info_section(tui, frame, chunks[2])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_chart_section(tui, frame, area)
|
|
63
|
+
max_value = [@data[:chart][:deltas][:processed].max, @data[:chart][:deltas][:failed].max, 1].max
|
|
64
|
+
y_max = [max_value, 5].max
|
|
65
|
+
|
|
66
|
+
processed_data = @data[:chart][:deltas][:processed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }
|
|
67
|
+
failed_data = @data[:chart][:deltas][:failed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }
|
|
68
|
+
|
|
69
|
+
datasets = [
|
|
70
|
+
tui.dataset(
|
|
71
|
+
name: "",
|
|
72
|
+
data: processed_data,
|
|
73
|
+
style: tui.style(fg: :green),
|
|
74
|
+
marker: :dot,
|
|
75
|
+
graph_type: :line
|
|
76
|
+
),
|
|
77
|
+
tui.dataset(
|
|
78
|
+
name: "",
|
|
79
|
+
data: failed_data,
|
|
80
|
+
style: tui.style(fg: :red),
|
|
81
|
+
marker: :dot,
|
|
82
|
+
graph_type: :line
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
num_labels = 5
|
|
87
|
+
y_labels = (0...num_labels).map do |i|
|
|
88
|
+
value = ((y_max * i) / (num_labels - 1)).round
|
|
89
|
+
value.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
beacon_pulse = (Time.now.to_i % 2 == 0) ? "●" : " "
|
|
93
|
+
|
|
94
|
+
chart = tui.chart(
|
|
95
|
+
datasets: datasets,
|
|
96
|
+
x_axis: tui.axis(
|
|
97
|
+
bounds: [0.0, 49.0],
|
|
98
|
+
labels: [],
|
|
99
|
+
style: tui.style(fg: :white)
|
|
100
|
+
),
|
|
101
|
+
y_axis: tui.axis(
|
|
102
|
+
bounds: [0.0, y_max.to_f],
|
|
103
|
+
labels: y_labels,
|
|
104
|
+
style: tui.style(fg: :white)
|
|
105
|
+
),
|
|
106
|
+
block: tui.block(
|
|
107
|
+
title: "Dashboard #{beacon_pulse}",
|
|
108
|
+
borders: [:all]
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
frame.render_widget(chart, area)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_redis_info_section(tui, frame, area)
|
|
116
|
+
redis_info = @data[:redis_info]
|
|
117
|
+
|
|
118
|
+
uptime_value = (redis_info[:uptime_days] == "N/A") ? "N/A" : "#{redis_info[:uptime_days]} days"
|
|
119
|
+
|
|
120
|
+
keys = ["Version", "Uptime", "Connected Clients", "Memory Usage", "Peak Memory"]
|
|
121
|
+
values = [
|
|
122
|
+
redis_info[:version].to_s,
|
|
123
|
+
uptime_value,
|
|
124
|
+
redis_info[:connected_clients].to_s,
|
|
125
|
+
redis_info[:used_memory].to_s,
|
|
126
|
+
redis_info[:peak_memory].to_s
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Format keys and values with spacing
|
|
130
|
+
keys_line = keys.map { |k| t(k).ljust(18) }.join(" ")
|
|
131
|
+
values_line = values.map { |v| v.ljust(18) }.join(" ")
|
|
132
|
+
|
|
133
|
+
frame.render_widget(
|
|
134
|
+
tui.paragraph(
|
|
135
|
+
text: [keys_line, values_line],
|
|
136
|
+
block: tui.block(title: "Redis Information", borders: [:all])
|
|
137
|
+
),
|
|
138
|
+
area
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|