Class: Sidekiq::TUI

Inherits:
Object
  • Object
show all
Includes:
Paginator
Defined in:
lib/sidekiq/tui.rb

Defined Under Namespace

Classes: PageOptions

Constant Summary collapse

REFRESH_INTERVAL_SECONDS =
2
TABS =
%w[Home Busy Queues Scheduled Retries Dead Metrics].freeze
CONTROLS =

CONTROLS defines data for input handling and for displaying controls. :code is the key code for input handling. :display and :description are shown in the controls area, with different

styling between them. If :display is omitted, :code is displayed instead.
Duplicate :display and :description values are ignored, shown only once.

:tabs is an array of tab names where the control is active. :action is a lambda to execute when the control is triggered.

Conventions: dangerous/irreversible actions should use UPPERCASE codes. The Shift button means “I’m sure”.

[
  {code: "?", display: "?", description: "Help", tabs: TABS,
   action: ->(tui) { tui.show_help }},
  {code: "left", display: "←/→", description: "Select Tab", tabs: TABS,
   action: ->(tui) { tui.navigate_tab(:left) }, refresh: true},
  {code: "right", display: "←/→", description: "Select Tab", tabs: TABS,
   action: ->(tui) { tui.navigate_tab(:right) }, refresh: true},
  {code: "q", display: "q", description: "Quit", tabs: TABS,
   action: ->(tui) { :quit }},
  {code: "c", modifiers: ["ctrl"], display: "q", description: "Quit", tabs: TABS,
   action: ->(tui) { :quit }},
  {code: "h", display: "h/l", description: "Prev/Next Page", tabs: TABS - ["Home"],
   action: ->(tui) { tui.prev_page }, refresh: true},
  {code: "l", display: "h/l", description: "Prev/Next Page", tabs: TABS - ["Home"],
   action: ->(tui) { tui.next_page }, refresh: true},
  {code: "k", display: "j/k", description: "Prev/Next Row", tabs: TABS - ["Home"],
   action: ->(tui) { tui.navigate_row(:up) }},
  {code: "j", display: "j/k", description: "Prev/Next Row", tabs: TABS - ["Home"],
   action: ->(tui) { tui.navigate_row(:down) }},
  {code: "x", display: "x", description: "Select", tabs: TABS - ["Home"],
   action: ->(tui) { tui.toggle_select }},
  {code: "A", modifiers: ["shift"], display: "A", description: "Select All", tabs: TABS - ["Home"],
   action: ->(tui) { tui.toggle_select(:all) }},
  {code: "D", modifiers: ["shift"], display: "D", description: "Delete", tabs: %w[Scheduled Retries Dead],
   action: ->(tui) { tui.alter_rows!(:delete) }, refresh: true},
  {code: "R", modifiers: ["shift"], display: "R", description: "Retry", tabs: %w[Retries],
   action: ->(tui) { tui.alter_rows!(:retry) }, refresh: true},
  {code: "E", modifiers: ["shift"], display: "E", description: "Enqueue", tabs: %w[Scheduled Dead],
   action: ->(tui) { tui.alter_rows!(:add_to_queue) }, refresh: true},
  {code: "K", modifiers: ["shift"], display: "K", description: "Kill", tabs: %w[Scheduled Retries],
   action: ->(tui) { tui.alter_rows!(:kill) }, refresh: true},
  {code: "D", modifiers: ["shift"], display: "D", description: "Delete", tabs: %w[Queues],
   action: ->(tui) { tui.delete_queue! }, refresh: true},
  {code: "p", description: "Pause/Unpause Queue", tabs: ["Queues"],
   action: ->(tui) { tui.toggle_pause_queue! }},
  {code: "T", modifiers: ["shift"], description: "Terminate", tabs: ["Busy"],
   action: ->(tui) { tui.terminate! }},
  {code: "Q", modifiers: ["shift"], description: "Quiet", tabs: ["Busy"],
   action: ->(tui) { tui.quiet! }},
  {code: "/", display: "/", description: "Filter", tabs: %w[Scheduled Retries Dead],
   action: ->(tui) { tui.start_filtering }}
].freeze
COLORS =
%i[blue cyan yellow red green white gray]

Constants included from Paginator

Paginator::TYPE_CACHE

Instance Method Summary collapse

Methods included from Paginator

#page, #page_items

Constructor Details

#initializeTUI

Returns a new instance of TUI.



88
89
90
91
92
93
94
95
# File 'lib/sidekiq/tui.rb', line 88

def initialize
  @current_tab = "Home"
  @selected_row_index = 0
  @base_style = nil
  @data = {}
  @last_refresh = Time.now
  @showing = :main
end

Instance Method Details

#alter_rows!(action = :add_to_queue) ⇒ Object



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/sidekiq/tui.rb', line 392

def alter_rows!(action = :add_to_queue)
  log(@current_tab, @data[:selected])
  set = case @current_tab
  when "Scheduled"
    Sidekiq::ScheduledSet.new
  when "Retries"
    Sidekiq::RetrySet.new
  when "Dead"
    Sidekiq::DeadSet.new
  end
  return unless set
  each_selection do |id|
    score, jid = id.split("|")
    item = set.fetch(score, jid)&.first
    item&.send(action)
  end
end

#data_for_set(set) ⇒ Object



578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/sidekiq/tui.rb', line 578

def data_for_set(set)
  f = @data[:filter]
  pager, rows, current, total = if f && f.size > 2
    rows = set.scan(f).to_a
    sz = rows.size
    [Sidekiq::TUI::PageOptions.new(1, sz), rows, 1, sz]
  else
    pager = @data.dig(:table, :pager) || Sidekiq::TUI::PageOptions.new(1, 25)
    current, total, items = page(set.name, pager.page, pager.size)
    rows = items.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
    [pager, rows, current, total]
  end

  @data.merge!(
    table: {pager:, rows:, current_page: current, total:,
            next_page: (current * pager.size < total) ? pager.page + 1 : nil,
            row_ids: rows.map { |job| [job.score, job["jid"]].join("|") }}
  )
end

#delete_queue!Object



386
387
388
389
390
# File 'lib/sidekiq/tui.rb', line 386

def delete_queue!
  each_selection do |qname|
    Sidekiq::Queue.new(qname).clear
  end
end

#each_selection(unselect: true) ⇒ Object



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/sidekiq/tui.rb', line 366

def each_selection(unselect: true, &)
  sel = @data[:selected]
  finished = []
  if !sel.empty?
    sel.each do |id|
      yield id
      # When processing multiple items in bulk, we want to unselect
      # each row if its operation succeeds so our UI will not
      # re-process rows 1-3 if row 4 fails.
      finished << id
    end
  else
    ids = @data.dig(:table, :row_ids)
    return if !ids || ids.empty?
    yield ids[@selected_row_index]
  end
ensure
  @data[:selected] = sel - finished if unselect
end

#format_memory(rss_kb) ⇒ Object



977
978
979
980
981
982
983
984
985
986
987
# File 'lib/sidekiq/tui.rb', line 977

def format_memory(rss_kb)
  return "0" if rss_kb.nil? || rss_kb == 0

  if rss_kb < 100_000
    "#{number_with_delimiter(rss_kb)} KB"
  elsif rss_kb < 10_000_000
    "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
  else
    "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
  end
end

#handle_inputObject



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/sidekiq/tui.rb', line 286

def handle_input
  case @tui.poll_event
  in {type: :key, code: "backspace"} if @data[:filtering]
    @data[:filter] = @data[:filter].empty? ? "" : @data[:filter][0..-2]
  in {type: :key, code: "enter"} if @data[:filtering]
    @data[:filtering] = nil
    @data[:selected] = []
  in {type: :key, code: "esc"} if @showing == :help
    @showing = :main
  in {type: :key, code: "esc"} if @data[:filtering]
    @data[:filtering] = nil
    @data[:filter] = nil
    @data[:selected] = []
  in {type: :key, code: code} if @data[:filtering] && code.length == 1
    @data[:filter] += code
    @data[:selected] = []
  in {type: :key, code:, modifiers:}
    control = CONTROLS.find { |ctrl|
      ctrl[:code] == code &&
        (ctrl[:modifiers] || []) == (modifiers || []) &&
        ctrl[:tabs].include?(@current_tab)
    }
    return unless control
    control[:action].call(self).tap {
      refresh_data if control[:refresh]
    }
  else
    # Ignore other events
  end
rescue => ex
  log(ex.message, ex.backtrace)
end

Navigate the row selection up or down in the current tab’s table.

Parameters:

  • direction (Symbol)

    :up or :down



337
338
339
340
341
342
343
# File 'lib/sidekiq/tui.rb', line 337

def navigate_row(direction)
  ids = @data.dig(:table, :row_ids)
  return if !ids || ids.empty?

  index_change = (direction == :down) ? 1 : -1
  @selected_row_index = (@selected_row_index + index_change) % ids.count
end

Navigate tabs to the left or right.

Parameters:

  • direction (Symbol)

    :left or :right



325
326
327
328
329
330
331
332
333
# File 'lib/sidekiq/tui.rb', line 325

def navigate_tab(direction)
  index_change = (direction == :right) ? 1 : -1
  @current_tab = TABS[(TABS.index(@current_tab) + index_change) % TABS.size]
  @selected_row_index = 0
  @data = {
    selected: [],
    filter: nil
  }
end

#next_pageObject



418
419
420
421
422
423
424
425
# File 'lib/sidekiq/tui.rb', line 418

def next_page
  np = @data.dig(:table, :next_page)
  return unless np
  opts = @data.dig(:table, :pager)
  return unless opts

  @data[:table][:pager] = Sidekiq::TUI::PageOptions.new(np, opts.size)
end

#number_with_delimiter(number, options = {}) ⇒ Object

TODO Implement I18n delimiter



972
973
974
975
# File 'lib/sidekiq/tui.rb', line 972

def number_with_delimiter(number, options = {})
  precision = options[:precision] || 0
  number.round(precision)
end

#prev_pageObject



410
411
412
413
414
415
416
# File 'lib/sidekiq/tui.rb', line 410

def prev_page
  opts = @data.dig(:table, :pager)
  return unless opts
  return if opts.page < 2

  @data[:table][:pager] = Sidekiq::TUI::PageOptions.new(opts.page - 1, opts.size)
end

#quiet!Object



354
355
356
357
358
# File 'lib/sidekiq/tui.rb', line 354

def quiet!
  each_selection do |id|
    Sidekiq::Process.new("identity" => id).quiet!
  end
end

#redis_urlObject



458
459
460
461
462
463
464
# File 'lib/sidekiq/tui.rb', line 458

def redis_url
  Sidekiq.redis do |conn|
    conn.config.server_url
  end
rescue
  "N/A"
end

#refresh_dataObject



470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/sidekiq/tui.rb', line 470

def refresh_data
  stats = Sidekiq::Stats.new
  @data[:stats] = {
    processed: stats.processed,
    failed: stats.failed,
    busy: stats.workers_size,
    enqueued: stats.enqueued,
    retries: stats.retry_size,
    scheduled: stats.scheduled_size,
    dead: stats.dead_size
  }

  case @current_tab
  when "Home"
    @data[:chart] ||= {
      previous_stats: {
        processed: stats.processed,
        failed: stats.failed
      },
      deltas: {
        processed: Array.new(50, 0),
        failed: Array.new(50, 0)
      }
    }

    processed_delta = stats.processed - @data[:chart][:previous_stats][:processed]
    failed_delta = stats.failed - @data[:chart][:previous_stats][:failed]

    @data[:chart][:deltas][:processed].shift
    @data[:chart][:deltas][:processed].push(processed_delta)
    @data[:chart][:deltas][:failed].shift
    @data[:chart][:deltas][:failed].push(failed_delta)

    @data[:chart][:previous_stats] = {
      processed: stats.processed,
      failed: stats.failed
    }

    redis_info = Sidekiq.default_configuration.redis_info

    @data[:redis_info] = {
      version: redis_info["redis_version"] || "N/A",
      uptime_days: redis_info["uptime_in_days"] || "N/A",
      connected_clients: redis_info["connected_clients"] || "N/A",
      used_memory: redis_info["used_memory_human"] || "N/A",
      peak_memory: redis_info["used_memory_peak_human"] || "N/A"
    }
  when "Busy"
    busy = []
    table_row_ids = []

    Sidekiq::ProcessSet.new.each do |p|
      name = "#{p["hostname"]}:#{p["pid"]}"
      name += " ⭐️" if p.leader?
      name += " 🛑" if p.stopping?
      busy << [
        selected?(p) ? "" : "",
        name,
        Time.at(p["started_at"]).utc,
        format_memory(p["rss"].to_i),
        number_with_delimiter(p["concurrency"]),
        number_with_delimiter(p["busy"])
      ]
      table_row_ids << p.identity
    end

    @data[:busy] = busy
    @data[:table] = {row_ids: table_row_ids}
  when "Queues"
    queue_summaries = Sidekiq::Stats.new.queue_summaries.sort_by(&:name)

    selected = Array(@data[:selected])
    queues = queue_summaries.map { |queue_summary|
      row_cells = [
        selected.index(queue_summary.name) ? "" : "",
        queue_summary.name,
        queue_summary.size.to_s,
        number_with_delimiter(queue_summary.latency, {precision: 2})
      ]
      row_cells << (queue_summary.paused? ? "" : "") if Sidekiq.pro?
      row_cells
    }

    table_row_ids = queue_summaries.map(&:name)

    @data[:queues] = queues
    @data[:table] = {row_ids: table_row_ids}
  when "Scheduled"
    data_for_set(Sidekiq::ScheduledSet.new)
  when "Retries"
    data_for_set(Sidekiq::RetrySet.new)
  when "Dead"
    data_for_set(Sidekiq::DeadSet.new)
  when "Metrics"
    # only need to refresh every 60 seconds
    if !@data[:metrics_refresh] || @data[:metrics_refresh] < Time.now
      q = Sidekiq::Metrics::Query.new
      query_result = q.top_jobs(minutes: 60)
      @data[:metrics] = query_result
      @data[:metrics_refresh] = Time.now + 60
    end
  end

  @last_refresh = Time.now
rescue => e
  @data = {error: e}
end

#renderObject



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/sidekiq/tui.rb', line 113

def render
  if @showing == :main
    @tui.draw do |frame|
      main_area, controls_area = @tui.layout_split(
        frame.area,
        direction: :vertical,
        constraints: [
          @tui.constraint_fill(1),
          @tui.constraint_length(4)
        ]
      )

      # Split main area into tabs and content
      tabs_area, content_area = @tui.layout_split(
        main_area,
        direction: :vertical,
        constraints: [
          @tui.constraint_length(3),
          @tui.constraint_fill(1)
        ]
      )

      tabs = @tui.tabs(
        titles: TABS,
        selected_index: TABS.index(@current_tab),
        block: @tui.block(title: Sidekiq::NAME, borders: [:all], title_style: @tui.style(fg: :red, modifiers: [:bold])),
        divider: " | ",
        highlight_style: @highlight_style,
        style: @base_style
      )
      frame.render_widget(tabs, tabs_area)

      render_content_area(frame, content_area)
      render_controls(frame, controls_area)
    end
  end

  if @showing == :help
    @tui.draw do |frame|
      main_area, controls_area = @tui.layout_split(
        frame.area,
        direction: :vertical,
        constraints: [
          @tui.constraint_fill(1),
          @tui.constraint_length(4)
        ]
      )
      content = @tui.block(
        title: Sidekiq::NAME,
        borders: [:all],
        title_style: @tui.style(fg: :red, modifiers: [:bold]),
        children: [
          # TODO convert to table
          @tui.paragraph(
            text: [
              @tui.text_line(spans: ["Welcome to the Sidekiq Terminal UI"], alignment: :center),
              @tui.text_line(spans: [
                @tui.text_span(content: "Esc", style: @hotkey_style),
                @tui.text_span(content: ": Close")
              ]),
              @tui.text_line(spans: [
                @tui.text_span(content: "←/→", style: @hotkey_style),
                @tui.text_span(content: ": Move between tabs")
              ]),
              @tui.text_line(spans: [
                @tui.text_span(content: "j/k", style: @hotkey_style),
                @tui.text_span(content: ": Use vim keys to move to prev/next row")
              ]),
              @tui.text_line(spans: [
                @tui.text_span(content: "x", style: @hotkey_style),
                @tui.text_span(content: ": Select/deselect current row")
              ]),
              @tui.text_line(spans: [
                @tui.text_span(content: "A", style: @hotkey_style),
                @tui.text_span(content: ": Select/deselect All visible rows")
              ]),
              @tui.text_line(spans: [
                @tui.text_span(content: "h/l", style: @hotkey_style),
                @tui.text_span(content: ": Use vim keys to move to prev/next page")
              ]),
              @tui.text_line(spans: [
                @tui.text_span(content: "q", style: @hotkey_style),
                @tui.text_span(content: ": Quit")
              ])
            ]
          )
        ]
      )
      frame.render_widget(content, main_area)
      controls = @tui.block(
        title: "Controls",
        borders: [:all],
        children: [
          @tui.paragraph(
            text: [
              @tui.text_line(spans: [
                @tui.text_span(content: "Esc", style: @hotkey_style),
                @tui.text_span(content: ": Close  ")
              ])
            ]
          )
        ]
      )
      frame.render_widget(controls, controls_area)
    end
  end
end

#render_busy(frame, area) ⇒ Object



598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/sidekiq/tui.rb', line 598

def render_busy(frame, area)
  chunks = @tui.layout_split(
    area,
    direction: :vertical,
    constraints: [
      @tui.constraint_length(4), # Stats
      @tui.constraint_length(4), # Status
      @tui.constraint_fill(1)   # Graph
    ]
  )

  render_stats_section(frame, chunks[0])
  render_status_section(frame, chunks[1])
  render_table(frame, chunks[2]) do
    {
      title: "Processes",
      header: ["☑️", "Name", "Started", "RSS", "Threads", "Busy"],
      widths: [
        @tui.constraint_length(5),
        @tui.constraint_fill(1),
        @tui.constraint_length(24),
        @tui.constraint_length(10),
        @tui.constraint_length(6),
        @tui.constraint_length(6)
      ],
      rows: @data[:busy].map.with_index { |cells, idx|
        @tui.table_row(
          cells:,
          style: idx.even? ? nil : @tui.style(bg: :dark_gray)
        )
      }
    }
  end
end

#render_chart_section(frame, area) ⇒ Object



746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
# File 'lib/sidekiq/tui.rb', line 746

def render_chart_section(frame, area)
  max_value = [@data[:chart][:deltas][:processed].max, @data[:chart][:deltas][:failed].max, 1].max
  y_max = [max_value, 5].max

  processed_data = @data[:chart][:deltas][:processed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }
  failed_data = @data[:chart][:deltas][:failed].each_with_index.map { |value, idx| [idx.to_f, value.to_f] }

  datasets = [
    @tui.dataset(
      name: "",
      data: processed_data,
      style: @tui.style(fg: :green),
      marker: :dot,
      graph_type: :line
    ),
    @tui.dataset(
      name: "",
      data: failed_data,
      style: @tui.style(fg: :red),
      marker: :dot,
      graph_type: :line
    )
  ]

  num_labels = 5
  y_labels = (0...num_labels).map do |i|
    value = ((y_max * i) / (num_labels - 1)).round
    value.to_s
  end

  beacon_pulse = (Time.now.to_i % 2 == 0) ? "" : " "

  chart = @tui.chart(
    datasets: datasets,
    x_axis: @tui.axis(
      bounds: [0.0, 49.0],
      labels: [],
      style: @tui.style(fg: :white)
    ),
    y_axis: @tui.axis(
      bounds: [0.0, y_max.to_f],
      labels: y_labels,
      style: @tui.style(fg: :white)
    ),
    block: @tui.block(
      title: "Dashboard #{beacon_pulse}",
      borders: [:all]
    )
  )

  frame.render_widget(chart, area)
end

#render_content_area(frame, content_area) ⇒ Object



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/sidekiq/tui.rb', line 221

def render_content_area(frame, content_area)
  return render_error(frame, content_area, @data[:error]) if @data[:error]

  case @current_tab
  when "Home"
    render_home(frame, content_area)
  when "Busy"
    render_busy(frame, content_area)
  when "Queues"
    render_queues(frame, content_area)
  when "Scheduled", "Retries", "Dead"
    render_set(frame, content_area)
  when "Metrics"
    render_metrics(frame, content_area)
  else
    frame.render_widget(
      @tui.paragraph(
        text: "Tab '#{@current_tab}' - Coming soon",
        alignment: :center,
        block: @tui.block(title: @current_tab, borders: [:all])
      ),
      content_area
    )
  end
end

#render_controls(frame, area) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/sidekiq/tui.rb', line 247

def render_controls(frame, area)
  keys_and_descriptions = CONTROLS
    .select { |ctrl|
      ctrl[:tabs].include?(@current_tab)
    }.map { |ctrl|
      [ctrl[:display] || ctrl[:code], ctrl[:description]]
    }.to_h

  controls = @tui.block(
    title: "Controls",
    borders: [:all],
    children: [
      @tui.paragraph(
        text: [
          @tui.text_line(spans: keys_and_descriptions.map { |key, desc|
            [
              @tui.text_span(content: key, style: @hotkey_style),
              @tui.text_span(content: ": #{desc}  ")
            ]
          }.flatten),
          # @tui.text_line(spans: [
          #   @tui.text_span(content: "d", style: @hotkey_style),
          #   @tui.text_span(content: ": Divider (#{@dividers[@divider_index]})  "),
          #   @tui.text_span(content: "s", style: @hotkey_style),
          #   @tui.text_span(content: ": Highlight (#{@highlight_styles[@highlight_style_index][:name]})  "),
          #   @tui.text_span(content: "b", style: @hotkey_style),
          #   @tui.text_span(content: ": Base Style (#{@base_styles[@base_style_index][:name]})  "),
          # ]),
          @tui.text_line(spans: [
            @tui.text_span(content: "Redis: #{redis_url} "),
            @tui.text_span(content: "Current Time: #{Time.now.utc}")
          ])
        ]
      )
    ]
  )
  frame.render_widget(controls, area)
end

#render_error(frame, area, err) ⇒ Object



953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
# File 'lib/sidekiq/tui.rb', line 953

def render_error(frame, area, err)
  log(err.message, err.backtrace)
  header = [@tui.text_line(
    spans: [@tui.text_span(content: err.message, style: @tui.style(modifiers: [:bold]))],
    alignment: :center
  )]
  lines = Array(err.backtrace).map { |line| @tui.text_line(spans: [@tui.text_span(content: line)]) }

  frame.render_widget(
    @tui.paragraph(
      text: header + lines,
      alignment: :left,
      block: @tui.block(title: "Error", borders: [:all], border_style: @tui.style(fg: :red))
    ),
    area
  )
end

#render_home(frame, area) ⇒ Object



677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
# File 'lib/sidekiq/tui.rb', line 677

def render_home(frame, area)
  chunks = @tui.layout_split(
    area,
    direction: :vertical,
    constraints: [
      @tui.constraint_length(4), # Stats
      @tui.constraint_fill(1),   # Graph
      @tui.constraint_length(4) # Redis
    ]
  )

  render_stats_section(frame, chunks[0])
  render_chart_section(frame, chunks[1])
  render_redis_info_section(frame, chunks[2])
end

#render_metrics(frame, area) ⇒ Object



857
858
859
860
861
862
863
864
865
866
867
868
869
870
# File 'lib/sidekiq/tui.rb', line 857

def render_metrics(frame, area)
  chunks = @tui.layout_split(
    area,
    direction: :vertical,
    constraints: [
      @tui.constraint_length(4), # Stats
      @tui.constraint_fill(1) # Chart
      # TOOD Table
    ]
  )

  render_stats_section(frame, chunks[0])
  render_metrics_chart(frame, chunks[1])
end

#render_metrics_chart(frame, area) ⇒ Object

Run to generate metrics data:

cd myapp && bundle install
bundle exec rake seed_jobs
bundle exec sidekiq


878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
# File 'lib/sidekiq/tui.rb', line 878

def render_metrics_chart(frame, area)
  y_max = 5
  csize = COLORS.size
  q = @data[:metrics]
  job_results = q.job_results.sort_by { |(kls, jr)| jr.totals["s"] }.reverse.first(COLORS.size)
  # visible_kls = job_results.first(5).map(&:first)
  # chart_data = {
  #   series: job_results.map { |(kls, jr)| [kls, jr.dig("series", "s")] }.to_h,
  #   marks: query_result.marks.map { |m| [m.bucket, m.label] },
  #   starts_at: query_result.starts_at.iso8601,
  #   ends_at: query_result.ends_at.iso8601,
  #   visibleKls: visible_kls,
  #   yLabel: 'TotalExecutionTime',
  #   units: 'seconds',
  #   markLabel: '*',
  # }

  datasets = job_results.map.with_index do |(kls, data), idx|
    # log kls, data, idx
    hrdata = data.dig("series", "s")
    tm = Time.now
    tmi = tm.to_i
    tm = Time.at(tmi - (tmi % 60)).utc
    data = Array.new(60) { |idx| idx }.map do |bucket_idx|
      jumpback = bucket_idx * 60
      value = hrdata[(tm - jumpback).iso8601] || 0
      y_max = value if value > y_max
      # we have 60 data points, newest data should be
      # at highest indexes so we have to rejigger the index
      # here
      [59 - bucket_idx, value]
    end
    # log data

    log(data)
    @tui.dataset(name: kls,
      data: data,
      style: @tui.style(fg: COLORS[idx % csize]),
      marker: :dot,
      graph_type: :line)
  end

  num_labels = 5
  y_labels = (0...num_labels).map do |i|
    value = ((y_max * i) / (num_labels - 1)).round
    value.to_s
  end
  xlabels = [
    q.starts_at.iso8601[11..15],
    q.ends_at.iso8601[11..15]
  ]

  # beacon_pulse = (Time.now.to_i % 2 == 0) ? "●" : " "

  chart = @tui.chart(
    datasets: datasets,
    x_axis: @tui.axis(
      bounds: [0.0, 60.0],
      labels: xlabels,
      style: @tui.style(fg: :white)
    ),
    y_axis: @tui.axis(
      bounds: [0.0, y_max.to_f],
      labels: y_labels,
      style: @tui.style(fg: :white)
    ),
    block: @tui.block(
      title: "Metrics",
      borders: [:all]
    )
  )

  frame.render_widget(chart, area)
end

#render_queues(frame, area) ⇒ Object



826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
# File 'lib/sidekiq/tui.rb', line 826

def render_queues(frame, area)
  header = ["☑️", "Queue", "Size", "Latency"]
  header << "Paused?" if Sidekiq.pro?

  chunks = @tui.layout_split(
    area,
    direction: :vertical,
    constraints: [
      @tui.constraint_length(4), # Stats
      @tui.constraint_fill(1) # Table
    ]
  )

  render_stats_section(frame, chunks[0])
  render_table(frame, chunks[1]) do
    {
      title: "Queues",
      header:,
      widths: header.map.with_index { |_, idx|
        @tui.constraint_length((idx == 1) ? 60 : 10)
      },
      rows: @data[:queues].map.with_index { |cells, idx|
        @tui.table_row(
          cells:,
          style: idx.even? ? nil : @tui.style(bg: :dark_gray)
        )
      }
    }
  end
end

#render_redis_info_section(frame, area) ⇒ Object



799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
# File 'lib/sidekiq/tui.rb', line 799

def render_redis_info_section(frame, area)
  redis_info = @data[:redis_info]

  uptime_value = (redis_info[:uptime_days] == "N/A") ? "N/A" : "#{redis_info[:uptime_days]} days"

  keys = ["Version", "Uptime", "Connected Clients", "Memory Usage", "Peak Memory"]
  values = [
    redis_info[:version].to_s,
    uptime_value,
    redis_info[:connected_clients].to_s,
    redis_info[:used_memory].to_s,
    redis_info[:peak_memory].to_s
  ]

  # Format keys and values with spacing
  keys_line = keys.map { |k| k.ljust(18) }.join("  ")
  values_line = values.map { |v| v.ljust(18) }.join("  ")

  frame.render_widget(
    @tui.paragraph(
      text: [keys_line, values_line],
      block: @tui.block(title: "Redis Information", borders: [:all])
    ),
    area
  )
end

#render_set(frame, area) ⇒ Object



633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
# File 'lib/sidekiq/tui.rb', line 633

def render_set(frame, area)
  chunks = @tui.layout_split(
    area,
    direction: :vertical,
    constraints: [
      @tui.constraint_length(4), # Stats
      @tui.constraint_fill(1)   # Table
    ]
  )

  render_stats_section(frame, chunks[0])
  render_table(frame, chunks[1]) do
    {
      title: @current_tab,
      header: ["☑️", "When", "Queue", "Job", "Arguments"],
      widths: [
        @tui.constraint_length(5),
        @tui.constraint_length(24),
        @tui.constraint_length(20),
        @tui.constraint_length(30),
        @tui.constraint_fill(1)
      ]
    }.tap do |h|
      rows = @data[:table][:rows].map.with_index { |entry, idx|
        @tui.table_row(
          cells: [
            selected?(entry) ? "" : "",
            entry.at,
            entry.queue,
            entry.display_class,
            entry.display_args
          ],
          style: idx.even? ? nil : @tui.style(bg: :dark_gray)
        )
      }
      h[:rows] = rows
    end
  end
end

#render_stats_section(frame, area) ⇒ Object



719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
# File 'lib/sidekiq/tui.rb', line 719

def render_stats_section(frame, area)
  stats = @data[:stats]

  keys = ["Processed", "Failed", "Busy", "Enqueued", "Retries", "Scheduled", "Dead"]
  values = [
    stats[:processed],
    stats[:failed],
    stats[:busy],
    stats[:enqueued],
    stats[:retries],
    stats[:scheduled],
    stats[:dead]
  ]

  # Format keys and values with spacing
  keys_line = keys.map { |k| k.to_s.ljust(12) }.join("  ")
  values_line = values.map { |v| v.to_s.ljust(12) }.join("  ")

  frame.render_widget(
    @tui.paragraph(
      text: [keys_line, values_line],
      block: @tui.block(title: "Statistics", borders: [:all])
    ),
    area
  )
end

#render_status_section(frame, area) ⇒ Object



693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
# File 'lib/sidekiq/tui.rb', line 693

def render_status_section(frame, area)
  keys = ["Processes", "Threads", "Busy", "Utilization", "RSS"]
  values = []
  processes = Sidekiq::ProcessSet.new
  workset = Sidekiq::WorkSet.new
  ws = workset.size
  values << (s = processes.size
             number_with_delimiter(s))
  values << (x = processes.total_concurrency
             number_with_delimiter(x))
  values << number_with_delimiter(ws)
  values << "#{(x == 0) ? 0 : ((ws / x.to_f) * 100).round(0)}%"
  values << format_memory(processes.total_rss)

  keys_line = keys.map { |k| k.to_s.ljust(12) }.join("  ")
  values_line = values.map { |v| v.to_s.ljust(12) }.join("  ")

  frame.render_widget(
    @tui.paragraph(
      text: [keys_line, values_line],
      block: @tui.block(title: "Status", borders: [:all])
    ),
    area
  )
end

#render_table(frame, area) ⇒ Object



989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
# File 'lib/sidekiq/tui.rb', line 989

def render_table(frame, area)
  page = @data.dig(:table, :current_page) || 1
  rows = @data.dig(:table, :rows) || []
  total = @data.dig(:table, :total) || 0
  footer = ["", "Page: #{page}", "Count: #{rows.size}", "Total: #{total}"]
  footer << "Selected: #{@data[:selected].size}" unless @data[:selected].empty?

  if @data[:filter]
    @filter_style = @tui.style(fg: :white, bg: :dark_gray)
    spans = [
      @tui.text_span(content: "Filter: ", style: @filter_style),
      @tui.text_span(content: @data[:filter], style: @filter_style)
    ]
    spans << @tui.text_span(content: "_", style: @tui.style(fg: :white, bg: :dark_gray, modifiers: [:slow_blink])) if @data[:filtering]
    footer << @tui.text_line(spans: spans)
  end

  defaults = {
    title: "TableName",
    highlight_symbol: "➡️",
    selected_row: @selected_row_index,
    row_highlight_style: @tui.style(fg: :white, bg: :blue),
    footer: footer
  }
  hash = defaults.merge(yield)
  hash[:block] ||= @tui.block(title: hash.delete(:title), borders: :all)
  table = @tui.table(**hash)
  frame.render_widget(table, area)
end

#runObject



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/sidekiq/tui.rb', line 97

def run
  RatatuiRuby.run do |tui|
    @tui = tui
    @highlight_style = @tui.style(fg: :red, modifiers: [:underlined])
    @hotkey_style = @tui.style(modifiers: [:bold, :underlined])

    refresh_data

    loop do
      refresh_data if should_refresh?
      render
      break if handle_input == :quit
    end
  end
end

#selected?(entry) ⇒ Boolean

Returns:

  • (Boolean)


673
674
675
# File 'lib/sidekiq/tui.rb', line 673

def selected?(entry)
  @data[:selected].index(entry.id)
end

#should_refresh?Boolean

Returns:

  • (Boolean)


466
467
468
# File 'lib/sidekiq/tui.rb', line 466

def should_refresh?
  Time.now - @last_refresh >= REFRESH_INTERVAL_SECONDS
end

#show_helpObject



319
320
321
# File 'lib/sidekiq/tui.rb', line 319

def show_help
  @showing = :help
end

#start_filteringObject



345
346
347
348
# File 'lib/sidekiq/tui.rb', line 345

def start_filtering
  @data[:filtering] = true
  @data[:filter] = ""
end

#stop_filteringObject



350
351
352
# File 'lib/sidekiq/tui.rb', line 350

def stop_filtering
  @data[:filtering] = false
end

#terminate!Object



360
361
362
363
364
# File 'lib/sidekiq/tui.rb', line 360

def terminate!
  each_selection do |id|
    Sidekiq::Process.new("identity" => id).stop!
  end
end

#toggle_pause_queue!Object



445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/sidekiq/tui.rb', line 445

def toggle_pause_queue!
  return unless Sidekiq.pro?

  each_selection do |qname|
    queue = Sidekiq::Queue.new(qname)
    if queue.paused?
      queue.unpause!
    else
      queue.pause!
    end
  end
end

#toggle_select(which = :current) ⇒ Object



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/sidekiq/tui.rb', line 427

def toggle_select(which = :current)
  sel = @data[:selected]
  log(which, sel)
  if which == :current
    x = @data[:table][:row_ids][@selected_row_index]
    if sel.index(x)
      # already checked, uncheck it
      sel.delete(x)
    else
      sel << x
    end
  elsif sel.empty?
    @data[:selected] = @data[:table][:row_ids]
  else
    sel.clear
  end
end