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.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +726 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +70 -39
  5. data/bin/kiq +17 -0
  6. data/bin/lint-herb +13 -0
  7. data/bin/multi_queue_bench +271 -0
  8. data/bin/sidekiq +4 -9
  9. data/bin/sidekiqload +214 -115
  10. data/bin/sidekiqmon +4 -1
  11. data/bin/webload +69 -0
  12. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  13. data/lib/generators/sidekiq/job_generator.rb +71 -0
  14. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
  15. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  16. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  17. data/lib/sidekiq/api.rb +729 -264
  18. data/lib/sidekiq/capsule.rb +135 -0
  19. data/lib/sidekiq/cli.rb +124 -100
  20. data/lib/sidekiq/client.rb +153 -106
  21. data/lib/sidekiq/component.rb +132 -0
  22. data/lib/sidekiq/config.rb +320 -0
  23. data/lib/sidekiq/deploy.rb +64 -0
  24. data/lib/sidekiq/embedded.rb +64 -0
  25. data/lib/sidekiq/fetch.rb +27 -26
  26. data/lib/sidekiq/iterable_job.rb +56 -0
  27. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  28. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  29. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  30. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  31. data/lib/sidekiq/job/iterable.rb +322 -0
  32. data/lib/sidekiq/job.rb +397 -5
  33. data/lib/sidekiq/job_logger.rb +23 -32
  34. data/lib/sidekiq/job_retry.rb +141 -68
  35. data/lib/sidekiq/job_util.rb +113 -0
  36. data/lib/sidekiq/launcher.rb +122 -98
  37. data/lib/sidekiq/loader.rb +57 -0
  38. data/lib/sidekiq/logger.rb +27 -106
  39. data/lib/sidekiq/manager.rb +41 -43
  40. data/lib/sidekiq/metrics/query.rb +184 -0
  41. data/lib/sidekiq/metrics/shared.rb +109 -0
  42. data/lib/sidekiq/metrics/tracking.rb +153 -0
  43. data/lib/sidekiq/middleware/chain.rb +96 -51
  44. data/lib/sidekiq/middleware/current_attributes.rb +120 -0
  45. data/lib/sidekiq/middleware/i18n.rb +8 -4
  46. data/lib/sidekiq/middleware/modules.rb +23 -0
  47. data/lib/sidekiq/monitor.rb +16 -6
  48. data/lib/sidekiq/paginator.rb +37 -10
  49. data/lib/sidekiq/processor.rb +105 -87
  50. data/lib/sidekiq/profiler.rb +73 -0
  51. data/lib/sidekiq/rails.rb +49 -36
  52. data/lib/sidekiq/redis_client_adapter.rb +117 -0
  53. data/lib/sidekiq/redis_connection.rb +55 -86
  54. data/lib/sidekiq/ring_buffer.rb +32 -0
  55. data/lib/sidekiq/scheduled.rb +106 -50
  56. data/lib/sidekiq/systemd.rb +2 -0
  57. data/lib/sidekiq/test_api.rb +331 -0
  58. data/lib/sidekiq/testing/inline.rb +2 -30
  59. data/lib/sidekiq/testing.rb +2 -342
  60. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  61. data/lib/sidekiq/tui/controls.rb +53 -0
  62. data/lib/sidekiq/tui/filtering.rb +53 -0
  63. data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  64. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  65. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  66. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  67. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  68. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  69. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  70. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  71. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  72. data/lib/sidekiq/tui/tabs.rb +15 -0
  73. data/lib/sidekiq/tui.rb +382 -0
  74. data/lib/sidekiq/version.rb +6 -1
  75. data/lib/sidekiq/web/action.rb +149 -64
  76. data/lib/sidekiq/web/application.rb +376 -268
  77. data/lib/sidekiq/web/config.rb +117 -0
  78. data/lib/sidekiq/web/helpers.rb +213 -87
  79. data/lib/sidekiq/web/router.rb +61 -74
  80. data/lib/sidekiq/web.rb +71 -100
  81. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  82. data/lib/sidekiq.rb +95 -196
  83. data/sidekiq.gemspec +14 -11
  84. data/web/assets/images/logo.png +0 -0
  85. data/web/assets/images/status.png +0 -0
  86. data/web/assets/javascripts/application.js +171 -57
  87. data/web/assets/javascripts/base-charts.js +120 -0
  88. data/web/assets/javascripts/chart.min.js +13 -0
  89. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  90. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  91. data/web/assets/javascripts/dashboard-charts.js +194 -0
  92. data/web/assets/javascripts/dashboard.js +41 -274
  93. data/web/assets/javascripts/metrics.js +280 -0
  94. data/web/assets/stylesheets/style.css +776 -0
  95. data/web/locales/ar.yml +72 -70
  96. data/web/locales/cs.yml +64 -62
  97. data/web/locales/da.yml +62 -53
  98. data/web/locales/de.yml +67 -65
  99. data/web/locales/el.yml +45 -24
  100. data/web/locales/en.yml +93 -69
  101. data/web/locales/es.yml +91 -68
  102. data/web/locales/fa.yml +67 -65
  103. data/web/locales/fr.yml +82 -67
  104. data/web/locales/gd.yml +110 -0
  105. data/web/locales/he.yml +67 -64
  106. data/web/locales/hi.yml +61 -59
  107. data/web/locales/it.yml +94 -54
  108. data/web/locales/ja.yml +74 -68
  109. data/web/locales/ko.yml +54 -52
  110. data/web/locales/lt.yml +68 -66
  111. data/web/locales/nb.yml +63 -61
  112. data/web/locales/nl.yml +54 -52
  113. data/web/locales/pl.yml +47 -45
  114. data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
  115. data/web/locales/pt.yml +53 -51
  116. data/web/locales/ru.yml +69 -66
  117. data/web/locales/sv.yml +55 -53
  118. data/web/locales/ta.yml +62 -60
  119. data/web/locales/tr.yml +102 -0
  120. data/web/locales/uk.yml +87 -61
  121. data/web/locales/ur.yml +66 -64
  122. data/web/locales/vi.yml +69 -67
  123. data/web/locales/zh-CN.yml +107 -0
  124. data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
  125. data/web/views/_footer.html.erb +32 -0
  126. data/web/views/_job_info.html.erb +115 -0
  127. data/web/views/_metrics_period_select.html.erb +15 -0
  128. data/web/views/_nav.html.erb +45 -0
  129. data/web/views/_paging.html.erb +26 -0
  130. data/web/views/_poll_link.html.erb +4 -0
  131. data/web/views/_summary.html.erb +40 -0
  132. data/web/views/busy.html.erb +151 -0
  133. data/web/views/dashboard.html.erb +104 -0
  134. data/web/views/dead.html.erb +38 -0
  135. data/web/views/filtering.html.erb +6 -0
  136. data/web/views/layout.html.erb +26 -0
  137. data/web/views/metrics.html.erb +85 -0
  138. data/web/views/metrics_for_job.html.erb +58 -0
  139. data/web/views/morgue.html.erb +69 -0
  140. data/web/views/profiles.html.erb +43 -0
  141. data/web/views/queue.html.erb +57 -0
  142. data/web/views/queues.html.erb +46 -0
  143. data/web/views/retries.html.erb +77 -0
  144. data/web/views/retry.html.erb +39 -0
  145. data/web/views/scheduled.html.erb +64 -0
  146. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  147. metadata +130 -61
  148. data/LICENSE +0 -9
  149. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  150. data/lib/sidekiq/delay.rb +0 -41
  151. data/lib/sidekiq/exception_handler.rb +0 -27
  152. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  153. data/lib/sidekiq/extensions/active_record.rb +0 -43
  154. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  155. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  156. data/lib/sidekiq/util.rb +0 -95
  157. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  158. data/lib/sidekiq/worker.rb +0 -244
  159. data/web/assets/stylesheets/application-dark.css +0 -147
  160. data/web/assets/stylesheets/application-rtl.css +0 -246
  161. data/web/assets/stylesheets/application.css +0 -1053
  162. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  163. data/web/assets/stylesheets/bootstrap.css +0 -5
  164. data/web/locales/zh-cn.yml +0 -68
  165. data/web/views/_footer.erb +0 -20
  166. data/web/views/_job_info.erb +0 -89
  167. data/web/views/_nav.erb +0 -52
  168. data/web/views/_paging.erb +0 -23
  169. data/web/views/_poll_link.erb +0 -7
  170. data/web/views/_status.erb +0 -4
  171. data/web/views/_summary.erb +0 -40
  172. data/web/views/busy.erb +0 -132
  173. data/web/views/dashboard.erb +0 -83
  174. data/web/views/dead.erb +0 -34
  175. data/web/views/layout.erb +0 -42
  176. data/web/views/morgue.erb +0 -78
  177. data/web/views/queue.erb +0 -55
  178. data/web/views/queues.erb +0 -38
  179. data/web/views/retries.erb +0 -83
  180. data/web/views/retry.erb +0 -34
  181. 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