sidekiq 8.1.0 → 8.1.2
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 +29 -0
- data/README.md +1 -1
- data/bin/kiq +17 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +11 -8
- data/lib/generators/sidekiq/job_generator.rb +15 -3
- data/lib/sidekiq/api.rb +130 -76
- data/lib/sidekiq/client.rb +3 -1
- data/lib/sidekiq/component.rb +3 -0
- data/lib/sidekiq/launcher.rb +1 -1
- data/lib/sidekiq/manager.rb +1 -1
- data/lib/sidekiq/paginator.rb +6 -1
- data/lib/sidekiq/profiler.rb +1 -1
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -334
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +187 -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 +380 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/application.rb +2 -2
- data/lib/sidekiq/web/helpers.rb +11 -0
- data/lib/sidekiq.rb +7 -0
- data/sidekiq.gemspec +1 -1
- data/web/assets/javascripts/application.js +1 -1
- data/web/locales/zh-TW.yml +1 -1
- data/web/views/busy.html.erb +1 -1
- metadata +18 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
|
|
3
|
+
module Sidekiq
|
|
4
|
+
class TUI
|
|
5
|
+
module Tabs
|
|
6
|
+
class Queues < BaseTab
|
|
7
|
+
def features
|
|
8
|
+
%i[selectable]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def controls
|
|
12
|
+
@controls ||= super + [
|
|
13
|
+
{code: "D", modifiers: ["shift"], display: "D", description: "Delete",
|
|
14
|
+
action: ->(tui, tab) { tab.delete_queue! }, refresh: true},
|
|
15
|
+
{code: "p", description: "Pause/Unpause Queue",
|
|
16
|
+
action: ->(tui, tab) { tab.toggle_pause_queue! }}
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def delete_queue!
|
|
21
|
+
each_selection do |qname|
|
|
22
|
+
Sidekiq::Queue.new(qname).clear
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def toggle_pause_queue!
|
|
27
|
+
return unless Sidekiq.pro?
|
|
28
|
+
|
|
29
|
+
each_selection do |qname|
|
|
30
|
+
queue = Sidekiq::Queue.new(qname)
|
|
31
|
+
if queue.paused?
|
|
32
|
+
queue.unpause!
|
|
33
|
+
else
|
|
34
|
+
queue.pause!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def refresh_data
|
|
40
|
+
refresh_data_for_stats
|
|
41
|
+
|
|
42
|
+
queue_summaries = Sidekiq::Stats.new.queue_summaries.sort_by(&:name)
|
|
43
|
+
|
|
44
|
+
selected = Array(@data[:selected])
|
|
45
|
+
queues = queue_summaries.map { |queue_summary|
|
|
46
|
+
row_cells = [
|
|
47
|
+
selected.index(queue_summary.name) ? "✅" : "",
|
|
48
|
+
queue_summary.name,
|
|
49
|
+
queue_summary.size.to_s,
|
|
50
|
+
number_with_delimiter(queue_summary.latency, {precision: 2})
|
|
51
|
+
]
|
|
52
|
+
row_cells << (queue_summary.paused? ? "✅" : "") if Sidekiq.pro?
|
|
53
|
+
row_cells
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
table_row_ids = queue_summaries.map(&:name)
|
|
57
|
+
|
|
58
|
+
@data[:queues] = queues
|
|
59
|
+
@data[:table] = {row_ids: table_row_ids}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render(tui, frame, area)
|
|
63
|
+
header = ["☑️", "Queue", "Size", "Latency"].map { |x| t(x) }
|
|
64
|
+
header << "Paused?" if Sidekiq.pro?
|
|
65
|
+
|
|
66
|
+
chunks = tui.layout_split(
|
|
67
|
+
area,
|
|
68
|
+
direction: :vertical,
|
|
69
|
+
constraints: [
|
|
70
|
+
tui.constraint_length(4), # Stats
|
|
71
|
+
tui.constraint_fill(1) # Table
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
render_stats_section(tui, frame, chunks[0])
|
|
76
|
+
render_table(tui, frame, chunks[1]) do
|
|
77
|
+
{
|
|
78
|
+
title: t(name),
|
|
79
|
+
header:,
|
|
80
|
+
widths: header.map.with_index { |_, idx|
|
|
81
|
+
tui.constraint_length((idx == 1) ? 60 : 10)
|
|
82
|
+
},
|
|
83
|
+
rows: @data[:queues].map.with_index { |cells, idx|
|
|
84
|
+
tui.table_row(
|
|
85
|
+
cells:,
|
|
86
|
+
style: idx.even? ? nil : tui.style(bg: :dark_gray)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
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 Retries < BaseTab
|
|
8
|
+
include SetTab
|
|
9
|
+
|
|
10
|
+
def set_class = Sidekiq::RetrySet
|
|
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,19 @@
|
|
|
1
|
+
require_relative "base_tab"
|
|
2
|
+
require_relative "set_tab"
|
|
3
|
+
|
|
4
|
+
module Sidekiq
|
|
5
|
+
class TUI
|
|
6
|
+
module Tabs
|
|
7
|
+
class Scheduled < BaseTab
|
|
8
|
+
include SetTab
|
|
9
|
+
|
|
10
|
+
def set_class = Sidekiq::ScheduledSet
|
|
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,96 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
class TUI
|
|
3
|
+
module Tabs
|
|
4
|
+
module SetTab
|
|
5
|
+
include Sidekiq::Paginator
|
|
6
|
+
include Filtering
|
|
7
|
+
|
|
8
|
+
def features
|
|
9
|
+
%i[selectable pageable filterable]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def controls
|
|
13
|
+
@controls ||= super + [{code: "D", modifiers: ["shift"], display: "D", description: "Delete",
|
|
14
|
+
action: ->(tui, tab) { tab.alter_rows!(:delete) }, refresh: true},
|
|
15
|
+
{code: "R", modifiers: ["shift"], display: "R", description: "Retry",
|
|
16
|
+
action: ->(tui, tab) { tab.alter_rows!(:retry) }, refresh: true},
|
|
17
|
+
{code: "E", modifiers: ["shift"], display: "E", description: "Enqueue",
|
|
18
|
+
action: ->(tui, tab) { tab.alter_rows!(:add_to_queue) }, refresh: true},
|
|
19
|
+
{code: "K", modifiers: ["shift"], display: "K", description: "Kill",
|
|
20
|
+
action: ->(tui, tab) { tab.alter_rows!(:kill) }, refresh: true}]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def alter_rows!(action)
|
|
24
|
+
# log(to_s, @data[:selected])
|
|
25
|
+
set = set_class.new
|
|
26
|
+
each_selection do |id|
|
|
27
|
+
score, jid = id.split("|")
|
|
28
|
+
item = set.fetch(score, jid)&.first
|
|
29
|
+
item&.send(action)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def refresh_data_for_set
|
|
34
|
+
set = set_class.new
|
|
35
|
+
f = current_filter
|
|
36
|
+
pager, rows, current, total = if f && f.size > 2
|
|
37
|
+
rows = set.scan(f).to_a
|
|
38
|
+
sz = rows.size
|
|
39
|
+
[Sidekiq::TUI::PageOptions.new(1, sz), rows, 1, sz]
|
|
40
|
+
else
|
|
41
|
+
pager = @data.dig(:table, :pager) || Sidekiq::TUI::PageOptions.new(1, 25)
|
|
42
|
+
current, total, items = page(set.name, pager.page, pager.size)
|
|
43
|
+
rows = items.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
|
44
|
+
[pager, rows, current, total]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@data.merge!(
|
|
48
|
+
table: {pager:, rows:, current_page: current, total:,
|
|
49
|
+
next_page: (current * pager.size < total) ? pager.page + 1 : nil,
|
|
50
|
+
row_ids: rows.map { |job| [job.score, job["jid"]].join("|") }}
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render(tui, frame, area)
|
|
55
|
+
chunks = tui.layout_split(
|
|
56
|
+
area,
|
|
57
|
+
direction: :vertical,
|
|
58
|
+
constraints: [
|
|
59
|
+
tui.constraint_length(4), # Stats
|
|
60
|
+
tui.constraint_fill(1) # Table
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
render_stats_section(tui, frame, chunks[0])
|
|
65
|
+
render_table(tui, frame, chunks[1]) do
|
|
66
|
+
{
|
|
67
|
+
title: t(name),
|
|
68
|
+
header: ["☑️", "When", "Queue", "Job", "Arguments"].map { |x| t(x) },
|
|
69
|
+
widths: [
|
|
70
|
+
tui.constraint_length(5),
|
|
71
|
+
tui.constraint_length(24),
|
|
72
|
+
tui.constraint_length(20),
|
|
73
|
+
tui.constraint_length(30),
|
|
74
|
+
tui.constraint_fill(1)
|
|
75
|
+
]
|
|
76
|
+
}.tap do |h|
|
|
77
|
+
rows = @data[:table][:rows].map.with_index { |entry, idx|
|
|
78
|
+
tui.table_row(
|
|
79
|
+
cells: [
|
|
80
|
+
selected?(entry) ? "✅" : "",
|
|
81
|
+
entry.at,
|
|
82
|
+
entry.queue,
|
|
83
|
+
entry.display_class,
|
|
84
|
+
entry.display_args
|
|
85
|
+
],
|
|
86
|
+
style: idx.even? ? nil : tui.style(bg: :dark_gray)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
h[:rows] = rows
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require_relative "tabs/home"
|
|
2
|
+
require_relative "tabs/busy"
|
|
3
|
+
require_relative "tabs/queues"
|
|
4
|
+
require_relative "tabs/scheduled"
|
|
5
|
+
require_relative "tabs/retries"
|
|
6
|
+
require_relative "tabs/dead"
|
|
7
|
+
require_relative "tabs/metrics"
|
|
8
|
+
|
|
9
|
+
module Sidekiq
|
|
10
|
+
class TUI
|
|
11
|
+
module Tabs
|
|
12
|
+
All = Set.new([Home, Busy, Queues, Scheduled, Retries, Dead, Metrics])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/sidekiq/tui.rb
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# https://sr.ht/~kerrick/ratatui_ruby/
|
|
2
|
+
# https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/examples/
|
|
3
|
+
gem "ratatui_ruby", ">=1.4.0"
|
|
4
|
+
require "ratatui_ruby"
|
|
5
|
+
|
|
6
|
+
RatatuiRuby.debug_mode! if !!ENV["DEBUG"]
|
|
7
|
+
|
|
8
|
+
require "sidekiq/api"
|
|
9
|
+
require "sidekiq/paginator"
|
|
10
|
+
|
|
11
|
+
require_relative "tui/filtering"
|
|
12
|
+
require_relative "tui/controls"
|
|
13
|
+
require_relative "tui/tabs"
|
|
14
|
+
|
|
15
|
+
module Sidekiq
|
|
16
|
+
class TUI
|
|
17
|
+
include Sidekiq::Component
|
|
18
|
+
|
|
19
|
+
PageOptions = Data.define(:page, :size)
|
|
20
|
+
|
|
21
|
+
REFRESH_INTERVAL_SECONDS = 2
|
|
22
|
+
LOCALE_DIRECTORIES = [File.expand_path("#{File.dirname(__FILE__)}/../../web/locales")]
|
|
23
|
+
|
|
24
|
+
# language is meant to be a locale code, e.g.
|
|
25
|
+
# LANG=en_US.utf-8
|
|
26
|
+
def initialize(cfg, language: ENV["LANG"] || "en")
|
|
27
|
+
@lang = language
|
|
28
|
+
@config = cfg
|
|
29
|
+
@base_style = nil
|
|
30
|
+
@last_refresh = Time.at(0)
|
|
31
|
+
@fps = Array.new(2) { 0 }
|
|
32
|
+
@previous_fps = 0
|
|
33
|
+
@showing = :main
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prepare(tui)
|
|
37
|
+
load_locale
|
|
38
|
+
|
|
39
|
+
@tui = tui
|
|
40
|
+
@highlight_style = @tui.style(fg: :light_red, modifiers: [:underlined])
|
|
41
|
+
@hotkey_style = @tui.style(modifiers: [:bold, :underlined])
|
|
42
|
+
# eager load tabs
|
|
43
|
+
all
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def run_loop
|
|
47
|
+
# Must log to a file, terminal is now controlled by Ratatui
|
|
48
|
+
config.logger = Logger.new("tui.log")
|
|
49
|
+
|
|
50
|
+
loop do
|
|
51
|
+
refresh_data if should_refresh?
|
|
52
|
+
render
|
|
53
|
+
break if handle_input == :quit
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render
|
|
58
|
+
track_fps do
|
|
59
|
+
if @showing == :main
|
|
60
|
+
@tui.draw do |frame|
|
|
61
|
+
main_area, controls_area = @tui.layout_split(
|
|
62
|
+
frame.area,
|
|
63
|
+
direction: :vertical,
|
|
64
|
+
constraints: [
|
|
65
|
+
@tui.constraint_fill(1),
|
|
66
|
+
@tui.constraint_length(5)
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Split main area into tabs and content
|
|
71
|
+
tabs_area, content_area = @tui.layout_split(
|
|
72
|
+
main_area,
|
|
73
|
+
direction: :vertical,
|
|
74
|
+
constraints: [
|
|
75
|
+
@tui.constraint_length(3),
|
|
76
|
+
@tui.constraint_fill(1)
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
all_tabs = all
|
|
81
|
+
tabs = @tui.tabs(
|
|
82
|
+
titles: all_tabs.map { |tab| t(tab.name) },
|
|
83
|
+
selected_index: all_tabs.index(current_tab),
|
|
84
|
+
block: @tui.block(title: " #{Sidekiq::NAME}", borders: [:all], title_style: @tui.style(fg: :light_red, modifiers: [:bold])),
|
|
85
|
+
divider: " | ",
|
|
86
|
+
highlight_style: @highlight_style,
|
|
87
|
+
style: @base_style
|
|
88
|
+
)
|
|
89
|
+
frame.render_widget(tabs, tabs_area)
|
|
90
|
+
|
|
91
|
+
render_content_area(frame, content_area)
|
|
92
|
+
render_controls(frame, controls_area)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if @showing == :help
|
|
97
|
+
@tui.draw do |frame|
|
|
98
|
+
main_area, controls_area = @tui.layout_split(
|
|
99
|
+
frame.area,
|
|
100
|
+
direction: :vertical,
|
|
101
|
+
constraints: [
|
|
102
|
+
@tui.constraint_fill(1),
|
|
103
|
+
@tui.constraint_length(4)
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
content = @tui.block(
|
|
107
|
+
title: " #{Sidekiq::NAME} ",
|
|
108
|
+
borders: [:all],
|
|
109
|
+
title_style: @tui.style(fg: :light_red, modifiers: [:bold]),
|
|
110
|
+
children: [
|
|
111
|
+
# TODO convert to table
|
|
112
|
+
@tui.paragraph(
|
|
113
|
+
text: [
|
|
114
|
+
@tui.text_line(spans: ["Welcome to the Sidekiq Terminal UI"], alignment: :center),
|
|
115
|
+
@tui.text_line(spans: [
|
|
116
|
+
@tui.text_span(content: "Global hotkeys")
|
|
117
|
+
]),
|
|
118
|
+
@tui.text_line(spans: []),
|
|
119
|
+
@tui.text_line(spans: [
|
|
120
|
+
@tui.text_span(content: "Esc", style: @hotkey_style),
|
|
121
|
+
@tui.text_span(content: ": Close this window")
|
|
122
|
+
]),
|
|
123
|
+
@tui.text_line(spans: [
|
|
124
|
+
@tui.text_span(content: "←/→", style: @hotkey_style),
|
|
125
|
+
@tui.text_span(content: ": Move between tabs")
|
|
126
|
+
]),
|
|
127
|
+
@tui.text_line(spans: [
|
|
128
|
+
@tui.text_span(content: "h/l", style: @hotkey_style),
|
|
129
|
+
@tui.text_span(content: ": Move to prev/next page of data")
|
|
130
|
+
]),
|
|
131
|
+
@tui.text_line(spans: [
|
|
132
|
+
@tui.text_span(content: "j/k", style: @hotkey_style),
|
|
133
|
+
@tui.text_span(content: ": Move to prev/next row in current page")
|
|
134
|
+
]),
|
|
135
|
+
@tui.text_line(spans: [
|
|
136
|
+
@tui.text_span(content: "x", style: @hotkey_style),
|
|
137
|
+
@tui.text_span(content: ": Select/deselect current row")
|
|
138
|
+
]),
|
|
139
|
+
@tui.text_line(spans: [
|
|
140
|
+
@tui.text_span(content: "A", style: @hotkey_style),
|
|
141
|
+
@tui.text_span(content: ": Select/deselect All rows in current page")
|
|
142
|
+
]),
|
|
143
|
+
@tui.text_line(spans: [
|
|
144
|
+
@tui.text_span(content: "q", style: @hotkey_style),
|
|
145
|
+
@tui.text_span(content: ": Quit")
|
|
146
|
+
])
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
frame.render_widget(content, main_area)
|
|
152
|
+
controls = @tui.block(
|
|
153
|
+
title: t("Controls"),
|
|
154
|
+
borders: [:all],
|
|
155
|
+
children: [
|
|
156
|
+
@tui.paragraph(
|
|
157
|
+
text: [
|
|
158
|
+
@tui.text_line(spans: [
|
|
159
|
+
@tui.text_span(content: "Esc", style: @hotkey_style),
|
|
160
|
+
@tui.text_span(content: ": Close ")
|
|
161
|
+
])
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
frame.render_widget(controls, controls_area)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def render_content_area(frame, content_area)
|
|
173
|
+
return render_error(frame, content_area, current_tab.error) if current_tab.error
|
|
174
|
+
|
|
175
|
+
current_tab.render(@tui, frame, content_area)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def render_controls(frame, area)
|
|
179
|
+
active_keys = current_tab.controls.filter { |hash| hash[:description] }
|
|
180
|
+
|
|
181
|
+
# Split controls into two lines, 8 is arbitrary
|
|
182
|
+
# TODO Dynamically split based on term width?
|
|
183
|
+
first = active_keys[...8]
|
|
184
|
+
lines = []
|
|
185
|
+
lines << @tui.text_line(spans: first.map { |hash|
|
|
186
|
+
[
|
|
187
|
+
@tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
|
|
188
|
+
@tui.text_span(content: ": #{t(hash[:description])} ")
|
|
189
|
+
]
|
|
190
|
+
}.flatten)
|
|
191
|
+
|
|
192
|
+
last = active_keys[8...]
|
|
193
|
+
lines << if last && last.size > 0
|
|
194
|
+
@tui.text_line(spans: last.map { |hash|
|
|
195
|
+
[
|
|
196
|
+
@tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
|
|
197
|
+
@tui.text_span(content: ": #{t(hash[:description])} ")
|
|
198
|
+
]
|
|
199
|
+
}.flatten)
|
|
200
|
+
else
|
|
201
|
+
@tui.text_line(spans: [])
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
footer = [
|
|
205
|
+
@tui.text_span(content: "Redis: #{redis_url} "),
|
|
206
|
+
@tui.text_span(content: "#{t("Now")}: #{Time.now.utc} "),
|
|
207
|
+
@tui.text_span(content: "#{t("Locale")}: #{@lang}")
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
if current_tab.data[:filter]
|
|
211
|
+
@filter_style = @tui.style(fg: :white, bg: :dark_gray)
|
|
212
|
+
footer += [
|
|
213
|
+
@tui.text_span(content: " #{t("Filter")}: ", style: @filter_style),
|
|
214
|
+
@tui.text_span(content: current_tab.data[:filter], style: @filter_style),
|
|
215
|
+
@tui.text_span(content: "_", style: @tui.style(fg: :white, bg: :dark_gray, modifiers: [:slow_blink]))
|
|
216
|
+
]
|
|
217
|
+
end
|
|
218
|
+
footer << @tui.text_span(content: " FPS: #{previous_fps}") if debugging?
|
|
219
|
+
lines << @tui.text_line(spans: footer)
|
|
220
|
+
|
|
221
|
+
controls = @tui.block(title: t("Controls"), borders: [:all],
|
|
222
|
+
children: [@tui.paragraph(text: lines)])
|
|
223
|
+
frame.render_widget(controls, area)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def handle_input
|
|
227
|
+
# We shouldn't need more than 10 FPS for a data-oriented app.
|
|
228
|
+
# This throttles down our CPU usage. Default is 60 FPS.
|
|
229
|
+
case @tui.poll_event(timeout: 0.1)
|
|
230
|
+
in {type: :key, code: "esc"} if @showing == :help
|
|
231
|
+
@showing = :main
|
|
232
|
+
in {type: :key, code: code} if current_tab.filtering? && code.length == 1
|
|
233
|
+
current_tab.append_to_filter(code)
|
|
234
|
+
current_tab.refresh_data
|
|
235
|
+
in {type: :key, code:, modifiers:}
|
|
236
|
+
control = current_tab.controls.find { |ctrl|
|
|
237
|
+
ctrl[:code] == code &&
|
|
238
|
+
(ctrl[:modifiers] || []) == (modifiers || [])
|
|
239
|
+
}
|
|
240
|
+
return unless control
|
|
241
|
+
control[:action].call(self, current_tab).tap {
|
|
242
|
+
refresh_data if control[:refresh]
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
# Ignore other events
|
|
246
|
+
end
|
|
247
|
+
rescue => ex
|
|
248
|
+
logger.error { [ex.message, ex.backtrace] }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def redis_url
|
|
252
|
+
Sidekiq.redis do |conn|
|
|
253
|
+
conn.config.server_url
|
|
254
|
+
end
|
|
255
|
+
rescue
|
|
256
|
+
"N/A"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def should_refresh?
|
|
260
|
+
Time.now - @last_refresh >= REFRESH_INTERVAL_SECONDS
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def refresh_data
|
|
264
|
+
# logger.info GC.stat
|
|
265
|
+
current_tab.refresh_data
|
|
266
|
+
@last_refresh = Time.now
|
|
267
|
+
rescue => e
|
|
268
|
+
handle_exception(e)
|
|
269
|
+
current_tab.error = e
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def render_error(frame, area, err)
|
|
273
|
+
header = [@tui.text_line(
|
|
274
|
+
spans: [@tui.text_span(content: err.message, style: @tui.style(modifiers: [:bold]))],
|
|
275
|
+
alignment: :center
|
|
276
|
+
)]
|
|
277
|
+
lines = Array(err.backtrace).map { |line| @tui.text_line(spans: [@tui.text_span(content: line)]) }
|
|
278
|
+
|
|
279
|
+
frame.render_widget(
|
|
280
|
+
@tui.paragraph(
|
|
281
|
+
text: header + lines,
|
|
282
|
+
alignment: :left,
|
|
283
|
+
block: @tui.block(title: t("Error"), borders: [:all], border_style: @tui.style(fg: :light_red))
|
|
284
|
+
),
|
|
285
|
+
area
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def show_help
|
|
290
|
+
@showing = :help
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def all
|
|
294
|
+
@all ||= Tabs::All.map { |kls| kls.new(self) }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def current_tab
|
|
298
|
+
@current ||= @all.first
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Navigate tabs to the left or right.
|
|
302
|
+
# @param direction [Symbol] :left or :right
|
|
303
|
+
def navigate(direction)
|
|
304
|
+
index_change = (direction == :right) ? 1 : -1
|
|
305
|
+
@current = @all[(@all.index(current_tab) + index_change) % @all.size]
|
|
306
|
+
@current.reset_data
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
public def t(msg, options = nil)
|
|
310
|
+
string = @strings[msg] || msg
|
|
311
|
+
if options.nil?
|
|
312
|
+
string
|
|
313
|
+
else
|
|
314
|
+
string % options
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def load_strings(lang)
|
|
319
|
+
{}.tap do |all|
|
|
320
|
+
find_locale_files(lang).each do |file|
|
|
321
|
+
strs = YAML.safe_load_file(file)
|
|
322
|
+
all.merge! strs[lang]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def locale_files
|
|
328
|
+
@@locale_files ||= LOCALE_DIRECTORIES.flat_map { |path|
|
|
329
|
+
Dir["#{path}/*.yml"]
|
|
330
|
+
}
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def available_locales
|
|
334
|
+
@@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def find_locale_files(lang)
|
|
338
|
+
locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def load_locale
|
|
342
|
+
require "yaml"
|
|
343
|
+
lang = @lang.split(".").first # "en_US"
|
|
344
|
+
while lang.size > 0
|
|
345
|
+
hash = load_strings(lang)
|
|
346
|
+
if hash.size > 0
|
|
347
|
+
# found a working language dataset
|
|
348
|
+
@lang = lang
|
|
349
|
+
@strings = hash
|
|
350
|
+
Sidekiq.logger.debug { "using the #{lang} locale" }
|
|
351
|
+
break
|
|
352
|
+
end
|
|
353
|
+
# Try "en_US", "en_U", "en_", "en"
|
|
354
|
+
# It's ugly and bruteforce but it works
|
|
355
|
+
lang = lang[..-2]
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def track_fps
|
|
360
|
+
# We hold two fps buckets: one for current second, one for previous second
|
|
361
|
+
idx = Time.now.to_i % 2
|
|
362
|
+
@fps[idx] += 1
|
|
363
|
+
yield
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def previous_fps
|
|
367
|
+
curidx = Time.now.to_i % 2
|
|
368
|
+
prev = (curidx == 1) ? 0 : 1
|
|
369
|
+
if (val = @fps[prev]) != 0
|
|
370
|
+
@previous_fps = val
|
|
371
|
+
@fps[prev] = 0
|
|
372
|
+
end
|
|
373
|
+
@previous_fps
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def debugging?
|
|
377
|
+
!!ENV["DEBUG"]
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
data/lib/sidekiq/version.rb
CHANGED
|
@@ -99,8 +99,8 @@ module Sidekiq
|
|
|
99
99
|
if url_params("identity")
|
|
100
100
|
pro = Sidekiq::ProcessSet[url_params("identity")]
|
|
101
101
|
|
|
102
|
-
pro
|
|
103
|
-
pro
|
|
102
|
+
pro&.quiet! if url_params("quiet")
|
|
103
|
+
pro&.stop! if url_params("stop")
|
|
104
104
|
else
|
|
105
105
|
processes.each do |pro|
|
|
106
106
|
next if pro.embedded?
|
data/lib/sidekiq/web/helpers.rb
CHANGED
|
@@ -287,6 +287,17 @@ module Sidekiq
|
|
|
287
287
|
%(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
+
def queue_names_by_capsule(pro)
|
|
291
|
+
cap = pro.capsules
|
|
292
|
+
if cap
|
|
293
|
+
cap.map { |k, v| v["weights"].keys.join(", ") }.join("; ")
|
|
294
|
+
else
|
|
295
|
+
# DEPRECATED Backwards compatibility with older processes.
|
|
296
|
+
# 'capsules' element added in v8.0.9
|
|
297
|
+
pro.queues.join(", ")
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
290
301
|
def job_params(job, score)
|
|
291
302
|
"#{score}-#{job["jid"]}"
|
|
292
303
|
end
|
data/lib/sidekiq.rb
CHANGED
|
@@ -47,6 +47,13 @@ module Sidekiq
|
|
|
47
47
|
puts "Take a deep breath and count to ten..."
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def self.testing!(mode = :fake, &block)
|
|
51
|
+
raise "Unknown testing mode: #{mode}" unless %i[fake disable inline].include?(mode)
|
|
52
|
+
|
|
53
|
+
require "sidekiq/test_api"
|
|
54
|
+
Sidekiq::Testing.__set_test_mode(mode, &block)
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
def self.server?
|
|
51
58
|
defined?(Sidekiq::CLI)
|
|
52
59
|
end
|
data/sidekiq.gemspec
CHANGED
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
|
|
|
8
8
|
gem.homepage = "https://sidekiq.org"
|
|
9
9
|
gem.license = "LGPL-3.0"
|
|
10
10
|
|
|
11
|
-
gem.executables = ["sidekiq", "sidekiqmon"]
|
|
11
|
+
gem.executables = ["sidekiq", "sidekiqmon", "kiq"]
|
|
12
12
|
gem.files = %w[sidekiq.gemspec README.md Changes.md LICENSE.txt] + `git ls-files | grep -E '^(bin|lib|web)'`.split("\n")
|
|
13
13
|
gem.name = "sidekiq"
|
|
14
14
|
gem.version = Sidekiq::VERSION
|