sidekiq 8.1.1 → 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 +17 -2
- data/README.md +1 -1
- data/bin/kiq +17 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +8 -8
- data/lib/sidekiq/api.rb +46 -35
- 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/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 +274 -913
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/helpers.rb +1 -1
- data/lib/sidekiq.rb +1 -1
- data/sidekiq.gemspec +1 -1
- data/web/locales/zh-TW.yml +1 -1
- metadata +15 -2
- data/bin/tui +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d21db4cf06c0b4d0b5fb770162e289403a76ed996d6c99613cf45083f0e671d
|
|
4
|
+
data.tar.gz: 07cf848cd5de112f3153ecd0014991cbf2abf9e66bc0847d2b7845ec7a726329
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 395d346f8b5227480e4d830b3c1f219a35bb0e2527283f9c00a6a4043c1ebe271998874ae53240b6e7dabca89ce8ad939d0a3969da2b33c3e4155fccbebae9ba
|
|
7
|
+
data.tar.gz: 20f44288fd8990544569af7f2a87d90d4ce898b2ce760cc95dd2f30c5a0c4ffc90bcc087cbfa22dbc188a904cc2e817756a83b5b3ffd648f75dc0413d3bec3ff
|
data/Changes.md
CHANGED
|
@@ -2,16 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
[Sidekiq Changes](https://github.com/sidekiq/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/sidekiq/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/sidekiq/sidekiq/blob/main/Ent-Changes.md)
|
|
4
4
|
|
|
5
|
+
8.1.2
|
|
6
|
+
----------
|
|
7
|
+
|
|
8
|
+
- Initial release for `kiq`, Sidekiq's official terminal UI:
|
|
9
|
+
```
|
|
10
|
+
bundle exec kiq
|
|
11
|
+
```
|
|
12
|
+
Use REDIS_URL or REDIS_PROVIDER to point `kiq` to Redis.
|
|
13
|
+
- Mutation during iteration in `SortedSet#each` caused it to miss half of the jobs [#6936]
|
|
14
|
+
- Fix edge case resulting in nil crash on /busy page [#6954]
|
|
15
|
+
|
|
5
16
|
8.1.1
|
|
6
17
|
----------
|
|
7
18
|
|
|
8
|
-
-
|
|
9
|
-
|
|
19
|
+
- **DEPRECATION** `require 'sidekiq/testing'` and
|
|
20
|
+
`require 'sidekiq/testing/inline'`.
|
|
21
|
+
Add new `Sidekiq.testing!(mode)` API [#6931]
|
|
22
|
+
Requiring code should not enable process-wide changes.
|
|
10
23
|
```ruby
|
|
11
24
|
# Old, implicit
|
|
12
25
|
require "sidekiq/testing"
|
|
26
|
+
require "sidekiq/testing/inline"
|
|
13
27
|
# New, more explicit
|
|
14
28
|
Sidekiq.testing!(:fake)
|
|
29
|
+
Sidekiq.testing!(:inline)
|
|
15
30
|
```
|
|
16
31
|
- Fix race condition with Stop button in UI [#6935]
|
|
17
32
|
- Fix javascript error handler [#6893]
|
data/README.md
CHANGED
|
@@ -90,7 +90,7 @@ Useful resources:
|
|
|
90
90
|
* The [Sidekiq tag](https://stackoverflow.com/questions/tagged/sidekiq) on Stack Overflow has lots of useful Q & A.
|
|
91
91
|
|
|
92
92
|
Every Thursday morning is Sidekiq Office Hour: I video chat and answer questions.
|
|
93
|
-
See the [Sidekiq support page](https://sidekiq.org/support
|
|
93
|
+
See the [Sidekiq support page](https://sidekiq.org/support/) for details.
|
|
94
94
|
|
|
95
95
|
Contributing
|
|
96
96
|
-----------------
|
data/bin/kiq
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# This requires the default gemset so Sidekiq Pro
|
|
4
|
+
# and Sidekiq Enterprise can load any code extensions.
|
|
5
|
+
Bundler.require(:default, :tui)
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/sidekiq/tui"
|
|
8
|
+
|
|
9
|
+
# Run any load hooks registered during Bundler.require
|
|
10
|
+
Sidekiq.loader.run_load_hooks(:tui)
|
|
11
|
+
|
|
12
|
+
tt = Sidekiq::TUI.new(Sidekiq.default_configuration)
|
|
13
|
+
|
|
14
|
+
RatatuiRuby.run do |tui|
|
|
15
|
+
tt.prepare(tui)
|
|
16
|
+
tt.run_loop
|
|
17
|
+
end
|
|
@@ -63,19 +63,19 @@ begin
|
|
|
63
63
|
def enqueue(job)
|
|
64
64
|
# NB: Active Job only serializes keys it recognizes. We
|
|
65
65
|
# cannot set arbitrary key/values here.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
66
|
+
options = {wrapped: job.class, queue: job.queue_name}
|
|
67
|
+
options[:profile] = job.profile if job.respond_to?(:profile) && !job.profile.nil?
|
|
68
|
+
|
|
69
|
+
wrapper = Sidekiq::ActiveJob::Wrapper.set(options)
|
|
70
70
|
job.provider_job_id = wrapper.perform_async(job.serialize)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# @api private
|
|
74
74
|
def enqueue_at(job, timestamp)
|
|
75
|
-
job.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
).perform_at(timestamp, job.serialize)
|
|
75
|
+
options = {wrapped: job.class, queue: job.queue_name}
|
|
76
|
+
options[:profile] = job.profile if job.respond_to?(:profile) && !job.profile.nil?
|
|
77
|
+
|
|
78
|
+
job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(options).perform_at(timestamp, job.serialize)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# @api private
|
data/lib/sidekiq/api.rb
CHANGED
|
@@ -657,38 +657,8 @@ module Sidekiq
|
|
|
657
657
|
|
|
658
658
|
private
|
|
659
659
|
|
|
660
|
-
def remove_job
|
|
661
|
-
|
|
662
|
-
results = conn.multi { |transaction|
|
|
663
|
-
transaction.zrange(parent.name, score, score, "BYSCORE")
|
|
664
|
-
transaction.zremrangebyscore(parent.name, score, score)
|
|
665
|
-
}.first
|
|
666
|
-
|
|
667
|
-
if results.size == 1
|
|
668
|
-
yield results.first
|
|
669
|
-
else
|
|
670
|
-
# multiple jobs with the same score
|
|
671
|
-
# find the one with the right JID and push it
|
|
672
|
-
matched, nonmatched = results.partition { |message|
|
|
673
|
-
if message.index(jid)
|
|
674
|
-
msg = Sidekiq.load_json(message)
|
|
675
|
-
msg["jid"] == jid
|
|
676
|
-
else
|
|
677
|
-
false
|
|
678
|
-
end
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
msg = matched.first
|
|
682
|
-
yield msg if msg
|
|
683
|
-
|
|
684
|
-
# push the rest back onto the sorted set
|
|
685
|
-
conn.multi do |transaction|
|
|
686
|
-
nonmatched.each do |message|
|
|
687
|
-
transaction.zadd(parent.name, score.to_f.to_s, message)
|
|
688
|
-
end
|
|
689
|
-
end
|
|
690
|
-
end
|
|
691
|
-
end
|
|
660
|
+
def remove_job(&)
|
|
661
|
+
parent.remove_job(self, &)
|
|
692
662
|
end
|
|
693
663
|
end
|
|
694
664
|
|
|
@@ -857,6 +827,46 @@ module Sidekiq
|
|
|
857
827
|
nil
|
|
858
828
|
end
|
|
859
829
|
|
|
830
|
+
def remove_job(entry)
|
|
831
|
+
score = entry.score
|
|
832
|
+
jid = entry.jid
|
|
833
|
+
Sidekiq.redis do |conn|
|
|
834
|
+
results = conn.multi { |transaction|
|
|
835
|
+
transaction.zrange(name, score, score, "BYSCORE")
|
|
836
|
+
transaction.zremrangebyscore(name, score, score)
|
|
837
|
+
}.first
|
|
838
|
+
|
|
839
|
+
if results.size == 1
|
|
840
|
+
yield results.first
|
|
841
|
+
@_size -= 1
|
|
842
|
+
else
|
|
843
|
+
# multiple jobs with the same score
|
|
844
|
+
# find the one with the right JID and push it
|
|
845
|
+
matched, nonmatched = results.partition { |message|
|
|
846
|
+
if message.index(jid)
|
|
847
|
+
msg = Sidekiq.load_json(message)
|
|
848
|
+
msg["jid"] == jid
|
|
849
|
+
else
|
|
850
|
+
false
|
|
851
|
+
end
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
msg = matched.first
|
|
855
|
+
if msg
|
|
856
|
+
yield msg
|
|
857
|
+
@_size -= 1
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# push the rest back onto the sorted set
|
|
861
|
+
conn.multi do |transaction|
|
|
862
|
+
nonmatched.each do |message|
|
|
863
|
+
transaction.zadd(name, score.to_f.to_s, message)
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
860
870
|
# :nodoc:
|
|
861
871
|
# @api private
|
|
862
872
|
def delete_by_value(name, value)
|
|
@@ -1030,19 +1040,20 @@ module Sidekiq
|
|
|
1030
1040
|
# you'll be happier this way
|
|
1031
1041
|
conn.pipelined do |pipeline|
|
|
1032
1042
|
procs.each do |key|
|
|
1033
|
-
pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
|
1043
|
+
pipeline.hmget(key, "info", "concurrency", "busy", "beat", "quiet", "rss", "rtt_us")
|
|
1034
1044
|
end
|
|
1035
1045
|
end
|
|
1036
1046
|
}
|
|
1037
1047
|
|
|
1038
|
-
result.each do |info, busy, beat, quiet, rss, rtt_us|
|
|
1048
|
+
result.each do |info, concurrency, busy, beat, quiet, rss, rtt_us|
|
|
1039
1049
|
# If a process is stopped between when we query Redis for `procs` and
|
|
1040
1050
|
# when we query for `result`, we will have an item in `result` that is
|
|
1041
1051
|
# composed of `nil` values.
|
|
1042
1052
|
next if info.nil?
|
|
1043
1053
|
|
|
1044
1054
|
hash = Sidekiq.load_json(info)
|
|
1045
|
-
yield Process.new(hash.merge("
|
|
1055
|
+
yield Process.new(hash.merge("concurrency" => concurrency.to_i,
|
|
1056
|
+
"busy" => busy.to_i,
|
|
1046
1057
|
"beat" => beat.to_f,
|
|
1047
1058
|
"quiet" => quiet,
|
|
1048
1059
|
"rss" => rss.to_i,
|
data/lib/sidekiq/client.rb
CHANGED
|
@@ -132,12 +132,14 @@ module Sidekiq
|
|
|
132
132
|
# push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
|
|
133
133
|
#
|
|
134
134
|
def push_bulk(items)
|
|
135
|
-
batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
|
|
136
135
|
args = items["args"]
|
|
137
136
|
at = items.delete("at") || items.delete(:at)
|
|
138
137
|
raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
|
|
139
138
|
raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
|
|
140
139
|
|
|
140
|
+
# Use a smaller batch size by default for scheduled jobs since adding to sorted sets is more costly.
|
|
141
|
+
batch_size = items.delete(:batch_size) || items.delete("batch_size") || (at ? 100 : 1_000)
|
|
142
|
+
|
|
141
143
|
jid = items.delete("jid")
|
|
142
144
|
raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
|
|
143
145
|
|
data/lib/sidekiq/component.rb
CHANGED
|
@@ -57,6 +57,9 @@ module Sidekiq
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def tid
|
|
60
|
+
# We XOR with PID to ensure Thread IDs changes after fork.
|
|
61
|
+
# I'm unclear why we don't multiply the two values to better guarantee
|
|
62
|
+
# a unique value but it's been this way for quite a while now. #3685
|
|
60
63
|
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
|
61
64
|
end
|
|
62
65
|
|
data/lib/sidekiq/launcher.rb
CHANGED
|
@@ -176,6 +176,7 @@ module Sidekiq
|
|
|
176
176
|
transaction.sadd("processes", [key])
|
|
177
177
|
transaction.exists(key)
|
|
178
178
|
transaction.hset(key, "info", to_json,
|
|
179
|
+
"concurrency", @config.total_concurrency,
|
|
179
180
|
"busy", curstate.size,
|
|
180
181
|
"beat", Time.now.to_f,
|
|
181
182
|
"rtt_us", rtt,
|
|
@@ -257,7 +258,6 @@ module Sidekiq
|
|
|
257
258
|
"started_at" => Time.now.to_f,
|
|
258
259
|
"pid" => ::Process.pid,
|
|
259
260
|
"tag" => @config[:tag] || "",
|
|
260
|
-
"concurrency" => @config.total_concurrency,
|
|
261
261
|
"capsules" => @config.capsules.each_with_object({}) { |(name, cap), memo|
|
|
262
262
|
memo[name] = cap.to_h
|
|
263
263
|
},
|
data/lib/sidekiq/manager.rb
CHANGED
data/lib/sidekiq/paginator.rb
CHANGED
|
@@ -62,7 +62,12 @@ module Sidekiq
|
|
|
62
62
|
pageidx = current_page - 1
|
|
63
63
|
starting = pageidx * page_size
|
|
64
64
|
items = items.to_a
|
|
65
|
-
|
|
65
|
+
total_size = items.size
|
|
66
|
+
if starting > total_size
|
|
67
|
+
starting = 0
|
|
68
|
+
current_page = 1
|
|
69
|
+
end
|
|
70
|
+
[current_page, total_size, items[starting, page_size]]
|
|
66
71
|
end
|
|
67
72
|
end
|
|
68
73
|
end
|
data/lib/sidekiq/profiler.rb
CHANGED
|
@@ -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,187 @@
|
|
|
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| v.to_s.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
|
+
# TODO Implement I18n delimiter
|
|
169
|
+
def number_with_delimiter(number, options = {})
|
|
170
|
+
precision = options[:precision] || 0
|
|
171
|
+
number.round(precision)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def format_memory(rss_kb)
|
|
175
|
+
return "0" if rss_kb.nil? || rss_kb == 0
|
|
176
|
+
|
|
177
|
+
if rss_kb < 100_000
|
|
178
|
+
"#{number_with_delimiter(rss_kb)} KB"
|
|
179
|
+
elsif rss_kb < 10_000_000
|
|
180
|
+
"#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
|
|
181
|
+
else
|
|
182
|
+
"#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|