sidekiq 8.1.1 → 8.1.3
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 +27 -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/capsule.rb +0 -1
- data/lib/sidekiq/cli.rb +1 -1
- data/lib/sidekiq/client.rb +3 -1
- data/lib/sidekiq/component.rb +3 -0
- data/lib/sidekiq/config.rb +1 -1
- 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/scheduled.rb +2 -5
- 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 +32 -3
- data/lib/sidekiq.rb +1 -1
- data/sidekiq.gemspec +1 -1
- data/web/assets/stylesheets/style.css +2 -0
- data/web/locales/ar.yml +1 -1
- data/web/locales/fa.yml +1 -1
- data/web/locales/gd.yml +1 -1
- data/web/locales/he.yml +1 -1
- data/web/locales/pt-BR.yml +1 -1
- data/web/locales/ur.yml +1 -1
- data/web/locales/zh-TW.yml +1 -1
- data/web/views/_paging.html.erb +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: 205ebd1ee2e6fdbe27b860b06a03147e96d77a3abc32481eb3bbf51ce01a64a6
|
|
4
|
+
data.tar.gz: 355195d5aadd03117e7eee7bdf144d193a4c9a41d4534b5f05ea6fad847cb7df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a592aa88c757173e70ee511388f15642e2aad1347df15252e972cea12e42ab91a0e8df702c3ff9e0069595bb68dae32a6d53318df390563a9ae7cc0df31852dd
|
|
7
|
+
data.tar.gz: d10b27d602e3c53fe79a85d21a59d0492831c32c8a67e2a344869c42e22e402ad591f82e3835e14c879a81fe8020430450fecd6a324d008bf33c4592dee23ea9
|
data/Changes.md
CHANGED
|
@@ -2,16 +2,41 @@
|
|
|
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.3
|
|
6
|
+
----------
|
|
7
|
+
|
|
8
|
+
- Fix edge case leading to duplicate, concurrent execution [#6379]
|
|
9
|
+
If 2 Capsules process jobs from the same queue, long-running
|
|
10
|
+
jobs could run in parallel during process shutdown.
|
|
11
|
+
- [SECURITY] Remove as much YAML usage as possible. [#6950]
|
|
12
|
+
Localization files in `web/locales` are now manually parsed.
|
|
13
|
+
Sidekiq::CLI will now only require YAML if you use a `-C` .yml file.
|
|
14
|
+
|
|
15
|
+
8.1.2
|
|
16
|
+
----------
|
|
17
|
+
|
|
18
|
+
- Initial release for `kiq`, Sidekiq's official terminal UI:
|
|
19
|
+
```
|
|
20
|
+
bundle exec kiq
|
|
21
|
+
```
|
|
22
|
+
Use REDIS_URL or REDIS_PROVIDER to point `kiq` to Redis.
|
|
23
|
+
- Mutation during iteration in `SortedSet#each` caused it to miss half of the jobs [#6936]
|
|
24
|
+
- Fix edge case resulting in nil crash on /busy page [#6954]
|
|
25
|
+
|
|
5
26
|
8.1.1
|
|
6
27
|
----------
|
|
7
28
|
|
|
8
|
-
-
|
|
9
|
-
|
|
29
|
+
- **DEPRECATION** `require 'sidekiq/testing'` and
|
|
30
|
+
`require 'sidekiq/testing/inline'`.
|
|
31
|
+
Add new `Sidekiq.testing!(mode)` API [#6931]
|
|
32
|
+
Requiring code should not enable process-wide changes.
|
|
10
33
|
```ruby
|
|
11
34
|
# Old, implicit
|
|
12
35
|
require "sidekiq/testing"
|
|
36
|
+
require "sidekiq/testing/inline"
|
|
13
37
|
# New, more explicit
|
|
14
38
|
Sidekiq.testing!(:fake)
|
|
39
|
+
Sidekiq.testing!(:inline)
|
|
15
40
|
```
|
|
16
41
|
- Fix race condition with Stop button in UI [#6935]
|
|
17
42
|
- 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/capsule.rb
CHANGED
data/lib/sidekiq/cli.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
$stdout.sync = true
|
|
4
4
|
|
|
5
|
-
require "yaml"
|
|
6
5
|
require "optparse"
|
|
7
6
|
require "erb"
|
|
8
7
|
require "fileutils"
|
|
@@ -409,6 +408,7 @@ module Sidekiq # :nodoc:
|
|
|
409
408
|
def parse_config(path)
|
|
410
409
|
erb = ERB.new(File.read(path), trim_mode: "-")
|
|
411
410
|
erb.filename = File.expand_path(path)
|
|
411
|
+
require "yaml"
|
|
412
412
|
opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
|
|
413
413
|
|
|
414
414
|
if opts.respond_to? :deep_symbolize_keys!
|
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/config.rb
CHANGED
|
@@ -255,7 +255,7 @@ module Sidekiq
|
|
|
255
255
|
# Register a proc to handle any error which occurs within the Sidekiq process.
|
|
256
256
|
#
|
|
257
257
|
# Sidekiq.configure_server do |config|
|
|
258
|
-
# config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
|
|
258
|
+
# config.error_handlers << proc {|ex,ctx_hash,config| MyErrorService.notify(ex, ctx_hash) }
|
|
259
259
|
# end
|
|
260
260
|
#
|
|
261
261
|
# The default error handler logs errors to @logger.
|
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
data/lib/sidekiq/scheduled.rb
CHANGED
|
@@ -117,9 +117,7 @@ module Sidekiq
|
|
|
117
117
|
private
|
|
118
118
|
|
|
119
119
|
def wait
|
|
120
|
-
@sleeper.pop(timeout: random_poll_interval)
|
|
121
|
-
rescue Timeout::Error
|
|
122
|
-
# TODO move to exception: false
|
|
120
|
+
@sleeper.pop(timeout: random_poll_interval, exception: false)
|
|
123
121
|
rescue => ex
|
|
124
122
|
# if poll_interval_average hasn't been calculated yet, we can
|
|
125
123
|
# raise an error trying to reach Redis.
|
|
@@ -225,8 +223,7 @@ module Sidekiq
|
|
|
225
223
|
total += INITIAL_WAIT unless @config[:poll_interval_average]
|
|
226
224
|
total += (5 * rand)
|
|
227
225
|
|
|
228
|
-
@sleeper.pop(timeout: total)
|
|
229
|
-
rescue Timeout::Error
|
|
226
|
+
@sleeper.pop(timeout: total, exception: false)
|
|
230
227
|
ensure
|
|
231
228
|
# periodically clean out the `processes` set in Redis which can collect
|
|
232
229
|
# references to dead processes over time. The process count affects how
|
|
@@ -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
|