solid_queue_tui 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/exe/sqtui +6 -0
- data/lib/solid_queue_tui/actions/discard_job.rb +34 -0
- data/lib/solid_queue_tui/actions/discard_scheduled_job.rb +33 -0
- data/lib/solid_queue_tui/actions/dispatch_scheduled_job.rb +35 -0
- data/lib/solid_queue_tui/actions/retry_job.rb +76 -0
- data/lib/solid_queue_tui/application.rb +468 -0
- data/lib/solid_queue_tui/cli.rb +48 -0
- data/lib/solid_queue_tui/components/header.rb +105 -0
- data/lib/solid_queue_tui/components/help_bar.rb +77 -0
- data/lib/solid_queue_tui/components/job_table.rb +122 -0
- data/lib/solid_queue_tui/connection.rb +58 -0
- data/lib/solid_queue_tui/data/failed_query.rb +118 -0
- data/lib/solid_queue_tui/data/jobs_query.rb +178 -0
- data/lib/solid_queue_tui/data/processes_query.rb +75 -0
- data/lib/solid_queue_tui/data/queues_query.rb +36 -0
- data/lib/solid_queue_tui/data/stats.rb +65 -0
- data/lib/solid_queue_tui/dev_reloader.rb +53 -0
- data/lib/solid_queue_tui/version.rb +5 -0
- data/lib/solid_queue_tui/views/blocked_view.rb +121 -0
- data/lib/solid_queue_tui/views/dashboard_view.rb +187 -0
- data/lib/solid_queue_tui/views/failed_view.rb +298 -0
- data/lib/solid_queue_tui/views/finished_view.rb +216 -0
- data/lib/solid_queue_tui/views/in_progress_view.rb +114 -0
- data/lib/solid_queue_tui/views/job_detail_view.rb +236 -0
- data/lib/solid_queue_tui/views/processes_view.rb +142 -0
- data/lib/solid_queue_tui/views/queues_view.rb +96 -0
- data/lib/solid_queue_tui/views/scheduled_view.rb +215 -0
- data/lib/solid_queue_tui.rb +46 -0
- metadata +157 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module SolidQueueTui
|
|
6
|
+
class CLI
|
|
7
|
+
def self.run(args)
|
|
8
|
+
options = parse_options(args)
|
|
9
|
+
Application.new(**options).run
|
|
10
|
+
rescue Connection::ConnectionError => e
|
|
11
|
+
$stderr.puts "Connection error: #{e.message}"
|
|
12
|
+
exit 1
|
|
13
|
+
rescue Interrupt
|
|
14
|
+
exit 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.parse_options(args)
|
|
18
|
+
options = {}
|
|
19
|
+
|
|
20
|
+
OptionParser.new do |opts|
|
|
21
|
+
opts.banner = "Usage: sqtui [options]"
|
|
22
|
+
opts.separator ""
|
|
23
|
+
opts.separator "Options:"
|
|
24
|
+
|
|
25
|
+
opts.on("--dev", "Enable hot-reload (watches lib/ for changes)") do
|
|
26
|
+
options[:dev] = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on("-v", "--version", "Show version") do
|
|
30
|
+
puts "sqtui v#{SolidQueueTui::VERSION}"
|
|
31
|
+
exit
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on("-h", "--help", "Show this help") do
|
|
35
|
+
puts opts
|
|
36
|
+
puts ""
|
|
37
|
+
puts "Configuration:"
|
|
38
|
+
puts " Create config/solid_tui.yml with:"
|
|
39
|
+
puts " database_url: sqlite3:storage/queue.sqlite3"
|
|
40
|
+
puts " refresh: 2"
|
|
41
|
+
exit
|
|
42
|
+
end
|
|
43
|
+
end.parse!(args)
|
|
44
|
+
|
|
45
|
+
options
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Components
|
|
5
|
+
class Header
|
|
6
|
+
LOGO = [
|
|
7
|
+
" ____ _ _ _ ___ ",
|
|
8
|
+
"/ ___| ___ | (_) __| | / _ \\ _ _ ___ _ _ ___ ",
|
|
9
|
+
"\\___ \\ / _ \\| | |/ _` | | | | | | | |/ _ \\ | | |/ _ \\",
|
|
10
|
+
" ___) | (_) | | | (_| | | |_| | |_| | __/ |_| | __/",
|
|
11
|
+
"|____/ \\___/|_|_|\\__,_| \\___/\\__,_|\\___|\\__,_|\\___|"
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
VIEWS = [
|
|
15
|
+
{ key: "1", label: "Dashboard" },
|
|
16
|
+
{ key: "2", label: "Queues" },
|
|
17
|
+
{ key: "3", label: "Failed" },
|
|
18
|
+
{ key: "4", label: "In Progress" },
|
|
19
|
+
{ key: "5", label: "Blocked" },
|
|
20
|
+
{ key: "6", label: "Scheduled" },
|
|
21
|
+
{ key: "7", label: "Finished" },
|
|
22
|
+
{ key: "8", label: "Workers" }
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(tui, current_view:)
|
|
26
|
+
@tui = tui
|
|
27
|
+
@current_view = current_view
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render(frame, area)
|
|
31
|
+
left_area, right_area = @tui.layout_split(
|
|
32
|
+
area,
|
|
33
|
+
direction: :horizontal,
|
|
34
|
+
constraints: [
|
|
35
|
+
@tui.constraint_percentage(50),
|
|
36
|
+
@tui.constraint_percentage(50)
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
render_info(frame, left_area)
|
|
41
|
+
render_nav(frame, right_area)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def render_info(frame, area)
|
|
47
|
+
lines = LOGO.map do |logo_line|
|
|
48
|
+
@tui.text_line(spans: [
|
|
49
|
+
@tui.text_span(content: " #{logo_line}", style: @tui.style(fg: :red, modifiers: [:bold]))
|
|
50
|
+
])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
lines << @tui.text_line(spans: [
|
|
54
|
+
@tui.text_span(content: " v#{VERSION}", style: @tui.style(fg: :dark_gray))
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
frame.render_widget(
|
|
58
|
+
@tui.paragraph(text: lines),
|
|
59
|
+
area
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_nav(frame, area)
|
|
64
|
+
spans = [
|
|
65
|
+
@tui.text_span(content: " ", style: @tui.style(fg: :white))
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
VIEWS.each_with_index do |view, idx|
|
|
69
|
+
active = idx == @current_view
|
|
70
|
+
|
|
71
|
+
spans << @tui.text_span(
|
|
72
|
+
content: "<#{view[:key]}>",
|
|
73
|
+
style: @tui.style(fg: :cyan, modifiers: active ? [:bold] : [])
|
|
74
|
+
)
|
|
75
|
+
spans << @tui.text_span(
|
|
76
|
+
content: " #{view[:label]}",
|
|
77
|
+
style: @tui.style(
|
|
78
|
+
fg: active ? :yellow : :dark_gray,
|
|
79
|
+
modifiers: active ? [:bold, :underlined] : []
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
spans << @tui.text_span(content: " ", style: @tui.style(fg: :white))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
lines = [
|
|
86
|
+
@tui.text_line(spans: spans, alignment: :right),
|
|
87
|
+
@tui.text_line(spans: [
|
|
88
|
+
@tui.text_span(content: "<?>", style: @tui.style(fg: :cyan)),
|
|
89
|
+
@tui.text_span(content: " Help ", style: @tui.style(fg: :dark_gray)),
|
|
90
|
+
@tui.text_span(content: "<q>", style: @tui.style(fg: :cyan)),
|
|
91
|
+
@tui.text_span(content: " Quit ", style: @tui.style(fg: :dark_gray)),
|
|
92
|
+
@tui.text_span(content: "<r>", style: @tui.style(fg: :cyan)),
|
|
93
|
+
@tui.text_span(content: " Refresh", style: @tui.style(fg: :dark_gray))
|
|
94
|
+
], alignment: :right)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
frame.render_widget(
|
|
98
|
+
@tui.paragraph(text: lines),
|
|
99
|
+
area
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Components
|
|
5
|
+
class HelpBar
|
|
6
|
+
DEFAULT_BINDINGS = [
|
|
7
|
+
{ key: "q", action: "Quit" },
|
|
8
|
+
{ key: "r", action: "Refresh" },
|
|
9
|
+
{ key: "Tab", action: "Next View" },
|
|
10
|
+
{ key: "j/k", action: "Navigate" },
|
|
11
|
+
{ key: "/", action: "Filter" },
|
|
12
|
+
{ key: "Esc", action: "Clear" }
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(tui, breadcrumb:, bindings: nil, status: nil)
|
|
16
|
+
@tui = tui
|
|
17
|
+
@breadcrumb = breadcrumb
|
|
18
|
+
@bindings = bindings || DEFAULT_BINDINGS
|
|
19
|
+
@status = status
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(frame, area)
|
|
23
|
+
left, right = @tui.layout_split(
|
|
24
|
+
area,
|
|
25
|
+
direction: :horizontal,
|
|
26
|
+
constraints: [
|
|
27
|
+
@tui.constraint_percentage(30),
|
|
28
|
+
@tui.constraint_percentage(70)
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
render_breadcrumb(frame, left)
|
|
33
|
+
render_bindings(frame, right)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def render_breadcrumb(frame, area)
|
|
39
|
+
spans = [
|
|
40
|
+
@tui.text_span(content: " <", style: @tui.style(fg: :dark_gray)),
|
|
41
|
+
@tui.text_span(content: @breadcrumb, style: @tui.style(fg: :yellow, modifiers: [:bold])),
|
|
42
|
+
@tui.text_span(content: ">", style: @tui.style(fg: :dark_gray))
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
if @status
|
|
46
|
+
spans << @tui.text_span(content: " #{@status}", style: @tui.style(fg: :dark_gray))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
frame.render_widget(
|
|
50
|
+
@tui.paragraph(text: [@tui.text_line(spans: spans)]),
|
|
51
|
+
area
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_bindings(frame, area)
|
|
56
|
+
spans = []
|
|
57
|
+
|
|
58
|
+
@bindings.each_with_index do |binding, idx|
|
|
59
|
+
spans << @tui.text_span(
|
|
60
|
+
content: binding[:key],
|
|
61
|
+
style: @tui.style(fg: :cyan, modifiers: [:bold])
|
|
62
|
+
)
|
|
63
|
+
spans << @tui.text_span(
|
|
64
|
+
content: ":#{binding[:action]}",
|
|
65
|
+
style: @tui.style(fg: :dark_gray)
|
|
66
|
+
)
|
|
67
|
+
spans << @tui.text_span(content: " ", style: @tui.style(fg: :white)) if idx < @bindings.size - 1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
frame.render_widget(
|
|
71
|
+
@tui.paragraph(text: [@tui.text_line(spans: spans, alignment: :right)]),
|
|
72
|
+
area
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Components
|
|
5
|
+
class JobTable
|
|
6
|
+
STATUS_COLORS = {
|
|
7
|
+
"ready" => :green,
|
|
8
|
+
"claimed" => :yellow,
|
|
9
|
+
"scheduled" => :blue,
|
|
10
|
+
"failed" => :red,
|
|
11
|
+
"blocked" => :magenta,
|
|
12
|
+
"completed" => :dark_gray,
|
|
13
|
+
"active" => :green,
|
|
14
|
+
"paused" => :red,
|
|
15
|
+
"unknown" => :white
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(tui, title:, columns:, rows:, selected_row: nil, empty_message: "No data")
|
|
19
|
+
@tui = tui
|
|
20
|
+
@title = title
|
|
21
|
+
@columns = columns
|
|
22
|
+
@rows = rows
|
|
23
|
+
@selected_row = selected_row
|
|
24
|
+
@empty_message = empty_message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render(frame, area, table_state)
|
|
28
|
+
if @rows.empty?
|
|
29
|
+
render_empty(frame, area)
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
render_table(frame, area, table_state)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def title_text
|
|
39
|
+
text = " #{@title} [#{@rows.size}]"
|
|
40
|
+
text += " #{@selected_row + 1}/#{@rows.size}" if @selected_row && @rows.size > 0
|
|
41
|
+
text + " "
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_table(frame, area, table_state)
|
|
45
|
+
widths = @columns.map do |col|
|
|
46
|
+
case col[:width]
|
|
47
|
+
when :fill then @tui.constraint_fill(1)
|
|
48
|
+
when Integer then @tui.constraint_length(col[:width])
|
|
49
|
+
else @tui.constraint_length(12)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
header = @columns.map { |col| col[:label] }
|
|
54
|
+
|
|
55
|
+
table = @tui.table(
|
|
56
|
+
rows: build_rows,
|
|
57
|
+
header: header,
|
|
58
|
+
widths: widths,
|
|
59
|
+
selected_row: @selected_row,
|
|
60
|
+
row_highlight_style: @tui.style(fg: :black, bg: :cyan, modifiers: [:bold]),
|
|
61
|
+
highlight_symbol: " > ",
|
|
62
|
+
highlight_spacing: :always,
|
|
63
|
+
column_spacing: 1,
|
|
64
|
+
style: @tui.style(fg: :white),
|
|
65
|
+
block: @tui.block(
|
|
66
|
+
title: title_text,
|
|
67
|
+
title_style: @tui.style(fg: :yellow, modifiers: [:bold]),
|
|
68
|
+
borders: [:all],
|
|
69
|
+
border_type: :rounded,
|
|
70
|
+
border_style: @tui.style(fg: :dark_gray)
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
frame.render_stateful_widget(table, area, table_state)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_rows
|
|
78
|
+
@rows.map do |row|
|
|
79
|
+
cells = @columns.map do |col|
|
|
80
|
+
value = row[col[:key]]
|
|
81
|
+
style = cell_style(col, value)
|
|
82
|
+
|
|
83
|
+
if style
|
|
84
|
+
@tui.table_cell(content: value.to_s, style: style)
|
|
85
|
+
else
|
|
86
|
+
value.to_s
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@tui.table_row(cells: cells)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def cell_style(col, value)
|
|
95
|
+
if col[:color_by] == :status
|
|
96
|
+
color = STATUS_COLORS[value.to_s.downcase] || :white
|
|
97
|
+
@tui.style(fg: color, modifiers: [:bold])
|
|
98
|
+
elsif col[:color]
|
|
99
|
+
@tui.style(fg: col[:color])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_empty(frame, area)
|
|
104
|
+
frame.render_widget(
|
|
105
|
+
@tui.paragraph(
|
|
106
|
+
text: @empty_message,
|
|
107
|
+
alignment: :center,
|
|
108
|
+
style: @tui.style(fg: :dark_gray),
|
|
109
|
+
block: @tui.block(
|
|
110
|
+
title: " #{@title} ",
|
|
111
|
+
title_style: @tui.style(fg: :yellow, modifiers: [:bold]),
|
|
112
|
+
borders: [:all],
|
|
113
|
+
border_type: :rounded,
|
|
114
|
+
border_style: @tui.style(fg: :dark_gray)
|
|
115
|
+
)
|
|
116
|
+
),
|
|
117
|
+
area
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module SolidQueueTui
|
|
7
|
+
class Connection
|
|
8
|
+
CONFIG_FILE = "config/solid_tui.yml"
|
|
9
|
+
|
|
10
|
+
# Loads config, establishes the DB connection, and returns the parsed config hash.
|
|
11
|
+
def self.establish!
|
|
12
|
+
config = load_config
|
|
13
|
+
|
|
14
|
+
url = config["database_url"]
|
|
15
|
+
raise ConnectionError, <<~MSG unless url
|
|
16
|
+
No database_url found in #{CONFIG_FILE}.
|
|
17
|
+
|
|
18
|
+
Create #{CONFIG_FILE} with:
|
|
19
|
+
database_url: postgres://user:pass@localhost/myapp_queue
|
|
20
|
+
refresh: 2
|
|
21
|
+
MSG
|
|
22
|
+
|
|
23
|
+
ActiveRecord::Base.establish_connection(url)
|
|
24
|
+
verify_solid_queue_tables!
|
|
25
|
+
|
|
26
|
+
config
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.load_config
|
|
30
|
+
path = File.join(Dir.pwd, CONFIG_FILE)
|
|
31
|
+
|
|
32
|
+
unless File.exist?(path)
|
|
33
|
+
raise ConnectionError, <<~MSG
|
|
34
|
+
Config file not found: #{CONFIG_FILE}
|
|
35
|
+
|
|
36
|
+
Create #{CONFIG_FILE} with:
|
|
37
|
+
database_url: postgres://user:pass@localhost/myapp_queue
|
|
38
|
+
refresh: 2
|
|
39
|
+
MSG
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
YAML.safe_load(File.read(path), permitted_classes: [Symbol]) || {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.verify_solid_queue_tables!
|
|
46
|
+
conn = ActiveRecord::Base.connection
|
|
47
|
+
required = %w[solid_queue_jobs solid_queue_ready_executions]
|
|
48
|
+
missing = required.reject { |t| conn.table_exists?(t) }
|
|
49
|
+
|
|
50
|
+
unless missing.empty?
|
|
51
|
+
raise ConnectionError, "Missing Solid Queue tables: #{missing.join(', ')}. " \
|
|
52
|
+
"Is this a Solid Queue database?"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class ConnectionError < StandardError; end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueTui
|
|
4
|
+
module Data
|
|
5
|
+
class FailedQuery
|
|
6
|
+
FailedJob = Struct.new(
|
|
7
|
+
:id, :job_id, :queue_name, :class_name, :priority,
|
|
8
|
+
:error_class, :error_message, :backtrace,
|
|
9
|
+
:active_job_id, :arguments, :failed_at, :created_at,
|
|
10
|
+
keyword_init: true
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def self.fetch(filter: nil, limit: 200)
|
|
14
|
+
conn = ActiveRecord::Base.connection
|
|
15
|
+
|
|
16
|
+
sql = <<~SQL
|
|
17
|
+
SELECT
|
|
18
|
+
fe.id,
|
|
19
|
+
fe.job_id,
|
|
20
|
+
j.queue_name,
|
|
21
|
+
j.class_name,
|
|
22
|
+
j.priority,
|
|
23
|
+
j.active_job_id,
|
|
24
|
+
j.arguments,
|
|
25
|
+
j.created_at AS job_created_at,
|
|
26
|
+
fe.error,
|
|
27
|
+
fe.created_at AS failed_at
|
|
28
|
+
FROM solid_queue_failed_executions fe
|
|
29
|
+
JOIN solid_queue_jobs j ON j.id = fe.job_id
|
|
30
|
+
SQL
|
|
31
|
+
|
|
32
|
+
if filter && !filter.empty?
|
|
33
|
+
sql += " WHERE j.class_name LIKE #{conn.quote("%#{filter}%")}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sql += " ORDER BY fe.created_at DESC LIMIT #{limit.to_i}"
|
|
37
|
+
|
|
38
|
+
rows = conn.select_all(sql)
|
|
39
|
+
rows.map do |row|
|
|
40
|
+
error = parse_json(row["error"])
|
|
41
|
+
|
|
42
|
+
FailedJob.new(
|
|
43
|
+
id: row["id"].to_i,
|
|
44
|
+
job_id: row["job_id"].to_i,
|
|
45
|
+
queue_name: row["queue_name"],
|
|
46
|
+
class_name: row["class_name"],
|
|
47
|
+
priority: row["priority"].to_i,
|
|
48
|
+
error_class: error["exception_class"] || error["class"] || "Unknown",
|
|
49
|
+
error_message: error["message"] || "No message",
|
|
50
|
+
backtrace: error["backtrace"] || [],
|
|
51
|
+
active_job_id: row["active_job_id"],
|
|
52
|
+
arguments: parse_json(row["arguments"]),
|
|
53
|
+
failed_at: parse_time(row["failed_at"]),
|
|
54
|
+
created_at: parse_time(row["job_created_at"])
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
rescue => e
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.fetch_one(id)
|
|
62
|
+
conn = ActiveRecord::Base.connection
|
|
63
|
+
|
|
64
|
+
row = conn.select_one(<<~SQL)
|
|
65
|
+
SELECT
|
|
66
|
+
fe.id,
|
|
67
|
+
fe.job_id,
|
|
68
|
+
j.queue_name,
|
|
69
|
+
j.class_name,
|
|
70
|
+
j.priority,
|
|
71
|
+
j.active_job_id,
|
|
72
|
+
j.arguments,
|
|
73
|
+
j.created_at AS job_created_at,
|
|
74
|
+
fe.error,
|
|
75
|
+
fe.created_at AS failed_at
|
|
76
|
+
FROM solid_queue_failed_executions fe
|
|
77
|
+
JOIN solid_queue_jobs j ON j.id = fe.job_id
|
|
78
|
+
WHERE fe.id = #{conn.quote(id.to_i)}
|
|
79
|
+
SQL
|
|
80
|
+
|
|
81
|
+
return nil unless row
|
|
82
|
+
|
|
83
|
+
error = parse_json(row["error"])
|
|
84
|
+
|
|
85
|
+
FailedJob.new(
|
|
86
|
+
id: row["id"].to_i,
|
|
87
|
+
job_id: row["job_id"].to_i,
|
|
88
|
+
queue_name: row["queue_name"],
|
|
89
|
+
class_name: row["class_name"],
|
|
90
|
+
priority: row["priority"].to_i,
|
|
91
|
+
error_class: error["exception_class"] || error["class"] || "Unknown",
|
|
92
|
+
error_message: error["message"] || "No message",
|
|
93
|
+
backtrace: error["backtrace"] || [],
|
|
94
|
+
active_job_id: row["active_job_id"],
|
|
95
|
+
arguments: parse_json(row["arguments"]),
|
|
96
|
+
failed_at: parse_time(row["failed_at"]),
|
|
97
|
+
created_at: parse_time(row["job_created_at"])
|
|
98
|
+
)
|
|
99
|
+
rescue => e
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private_class_method def self.parse_json(value)
|
|
104
|
+
return {} if value.nil?
|
|
105
|
+
value.is_a?(Hash) || value.is_a?(Array) ? value : JSON.parse(value.to_s)
|
|
106
|
+
rescue JSON::ParserError
|
|
107
|
+
{}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private_class_method def self.parse_time(value)
|
|
111
|
+
return nil if value.nil?
|
|
112
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
113
|
+
rescue
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|