solid_job_monitor 1.0.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/MIT-LICENSE +20 -0
- data/README.md +106 -0
- data/Rakefile +8 -0
- data/app/assets/config/mission_control_jobs_manifest.js +4 -0
- data/app/assets/stylesheets/mission_control/jobs/application.css +16 -0
- data/app/assets/stylesheets/mission_control/jobs/bulma.min.css +21013 -0
- data/app/assets/stylesheets/mission_control/jobs/forms.css +8 -0
- data/app/assets/stylesheets/mission_control/jobs/jobs.css +68 -0
- data/app/controllers/concerns/mission_control/jobs/adapter_features.rb +24 -0
- data/app/controllers/concerns/mission_control/jobs/application_scoped.rb +38 -0
- data/app/controllers/concerns/mission_control/jobs/basic_authentication.rb +33 -0
- data/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb +17 -0
- data/app/controllers/concerns/mission_control/jobs/job_filters.rb +41 -0
- data/app/controllers/concerns/mission_control/jobs/job_scoped.rb +16 -0
- data/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb +36 -0
- data/app/controllers/concerns/mission_control/jobs/queue_scoped.rb +12 -0
- data/app/controllers/mission_control/jobs/application_controller.rb +32 -0
- data/app/controllers/mission_control/jobs/bulk_discards_controller.rb +20 -0
- data/app/controllers/mission_control/jobs/bulk_retries_controller.rb +10 -0
- data/app/controllers/mission_control/jobs/discards_controller.rb +18 -0
- data/app/controllers/mission_control/jobs/dispatches_controller.rb +18 -0
- data/app/controllers/mission_control/jobs/jobs_controller.rb +54 -0
- data/app/controllers/mission_control/jobs/progresses_controller.rb +27 -0
- data/app/controllers/mission_control/jobs/queues/pauses_controller.rb +15 -0
- data/app/controllers/mission_control/jobs/queues_controller.rb +16 -0
- data/app/controllers/mission_control/jobs/recurring_tasks_controller.rb +38 -0
- data/app/controllers/mission_control/jobs/retries_controller.rb +13 -0
- data/app/controllers/mission_control/jobs/stops_controller.rb +22 -0
- data/app/controllers/mission_control/jobs/workers_controller.rb +23 -0
- data/app/helpers/mission_control/jobs/application_helper.rb +9 -0
- data/app/helpers/mission_control/jobs/dates_helper.rb +19 -0
- data/app/helpers/mission_control/jobs/interface_helper.rb +23 -0
- data/app/helpers/mission_control/jobs/jobs_helper.rb +94 -0
- data/app/helpers/mission_control/jobs/navigation_helper.rb +60 -0
- data/app/javascript/mission_control/jobs/application.js +4 -0
- data/app/javascript/mission_control/jobs/controllers/application.js +9 -0
- data/app/javascript/mission_control/jobs/controllers/form_controller.js +21 -0
- data/app/javascript/mission_control/jobs/controllers/index.js +11 -0
- data/app/javascript/mission_control/jobs/controllers/progress_controller.js +55 -0
- data/app/javascript/mission_control/jobs/helpers/debounce_helpers.js +9 -0
- data/app/javascript/mission_control/jobs/helpers/index.js +1 -0
- data/app/models/mission_control/jobs/application_record.rb +7 -0
- data/app/models/mission_control/jobs/current.rb +3 -0
- data/app/models/mission_control/jobs/job_progress.rb +7 -0
- data/app/models/mission_control/jobs/page.rb +48 -0
- data/app/models/mission_control/jobs/recurring_task.rb +25 -0
- data/app/models/mission_control/jobs/worker.rb +17 -0
- data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +14 -0
- data/app/views/layouts/mission_control/jobs/_flash.html.erb +9 -0
- data/app/views/layouts/mission_control/jobs/_navigation.html.erb +9 -0
- data/app/views/layouts/mission_control/jobs/application.html.erb +27 -0
- data/app/views/layouts/mission_control/jobs/application_selection/_applications.html.erb +13 -0
- data/app/views/layouts/mission_control/jobs/application_selection/_servers.html.erb +15 -0
- data/app/views/mission_control/jobs/jobs/_error_information.html.erb +22 -0
- data/app/views/mission_control/jobs/jobs/_filters.html.erb +55 -0
- data/app/views/mission_control/jobs/jobs/_general_information.html.erb +85 -0
- data/app/views/mission_control/jobs/jobs/_job.html.erb +13 -0
- data/app/views/mission_control/jobs/jobs/_jobs_page.html.erb +13 -0
- data/app/views/mission_control/jobs/jobs/_progress_bar.html.erb +16 -0
- data/app/views/mission_control/jobs/jobs/_raw_data.html.erb +4 -0
- data/app/views/mission_control/jobs/jobs/_title.html.erb +22 -0
- data/app/views/mission_control/jobs/jobs/_toolbar.html.erb +18 -0
- data/app/views/mission_control/jobs/jobs/blocked/_actions.html.erb +3 -0
- data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +8 -0
- data/app/views/mission_control/jobs/jobs/failed/_actions.html.erb +5 -0
- data/app/views/mission_control/jobs/jobs/failed/_backtrace_toggle.html.erb +14 -0
- data/app/views/mission_control/jobs/jobs/failed/_job.html.erb +7 -0
- data/app/views/mission_control/jobs/jobs/finished/_job.html.erb +2 -0
- data/app/views/mission_control/jobs/jobs/in_progress/_actions.html.erb +6 -0
- data/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +13 -0
- data/app/views/mission_control/jobs/jobs/index.html.erb +19 -0
- data/app/views/mission_control/jobs/jobs/scheduled/_actions.html.erb +5 -0
- data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +13 -0
- data/app/views/mission_control/jobs/jobs/show.html.erb +6 -0
- data/app/views/mission_control/jobs/queues/_actions.html.erb +7 -0
- data/app/views/mission_control/jobs/queues/_job.html.erb +15 -0
- data/app/views/mission_control/jobs/queues/_queue.html.erb +16 -0
- data/app/views/mission_control/jobs/queues/_queue_title.html.erb +17 -0
- data/app/views/mission_control/jobs/queues/index.html.erb +16 -0
- data/app/views/mission_control/jobs/queues/show.html.erb +25 -0
- data/app/views/mission_control/jobs/recurring_tasks/_actions.html.erb +5 -0
- data/app/views/mission_control/jobs/recurring_tasks/_general_information.html.erb +37 -0
- data/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb +22 -0
- data/app/views/mission_control/jobs/recurring_tasks/_title.html.erb +10 -0
- data/app/views/mission_control/jobs/recurring_tasks/index.html.erb +18 -0
- data/app/views/mission_control/jobs/recurring_tasks/show.html.erb +14 -0
- data/app/views/mission_control/jobs/shared/_job.html.erb +27 -0
- data/app/views/mission_control/jobs/shared/_jobs.html.erb +14 -0
- data/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb +7 -0
- data/app/views/mission_control/jobs/workers/_configuration.html.erb +6 -0
- data/app/views/mission_control/jobs/workers/_raw_data.html.erb +6 -0
- data/app/views/mission_control/jobs/workers/_title.html.erb +11 -0
- data/app/views/mission_control/jobs/workers/_worker.html.erb +21 -0
- data/app/views/mission_control/jobs/workers/_workers_page.html.erb +15 -0
- data/app/views/mission_control/jobs/workers/index.html.erb +6 -0
- data/app/views/mission_control/jobs/workers/show.html.erb +14 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20260828105144_create_mission_control_job_progresses.rb +16 -0
- data/lib/active_job/errors/invalid_operation.rb +5 -0
- data/lib/active_job/errors/job_not_found_error.rb +14 -0
- data/lib/active_job/errors/query_error.rb +5 -0
- data/lib/active_job/executing.rb +40 -0
- data/lib/active_job/execution_error.rb +8 -0
- data/lib/active_job/failed.rb +7 -0
- data/lib/active_job/job_proxy.rb +38 -0
- data/lib/active_job/jobs_relation.rb +318 -0
- data/lib/active_job/progress_reporting.rb +21 -0
- data/lib/active_job/querying.rb +44 -0
- data/lib/active_job/queue.rb +62 -0
- data/lib/active_job/queue_adapters/async_ext.rb +49 -0
- data/lib/active_job/queue_adapters/resque_ext.rb +317 -0
- data/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb +52 -0
- data/lib/active_job/queue_adapters/solid_queue_ext/workers.rb +41 -0
- data/lib/active_job/queue_adapters/solid_queue_ext.rb +313 -0
- data/lib/active_job/queues.rb +33 -0
- data/lib/mission_control/jobs/adapter.rb +156 -0
- data/lib/mission_control/jobs/application.rb +20 -0
- data/lib/mission_control/jobs/applications.rb +8 -0
- data/lib/mission_control/jobs/arguments_filter.rb +24 -0
- data/lib/mission_control/jobs/authentication.rb +67 -0
- data/lib/mission_control/jobs/console/connect_to.rb +15 -0
- data/lib/mission_control/jobs/console/context.rb +11 -0
- data/lib/mission_control/jobs/console/jobs_help.rb +23 -0
- data/lib/mission_control/jobs/engine.rb +118 -0
- data/lib/mission_control/jobs/errors/incompatible_adapter.rb +2 -0
- data/lib/mission_control/jobs/errors/resource_not_found.rb +2 -0
- data/lib/mission_control/jobs/i18n_config.rb +17 -0
- data/lib/mission_control/jobs/identified_by_name.rb +18 -0
- data/lib/mission_control/jobs/identified_elements.rb +23 -0
- data/lib/mission_control/jobs/job_stopped_error.rb +5 -0
- data/lib/mission_control/jobs/progress/null_store.rb +25 -0
- data/lib/mission_control/jobs/progress/reporter.rb +63 -0
- data/lib/mission_control/jobs/progress/store.rb +72 -0
- data/lib/mission_control/jobs/progress.rb +17 -0
- data/lib/mission_control/jobs/server/recurring_tasks.rb +15 -0
- data/lib/mission_control/jobs/server/serializable.rb +24 -0
- data/lib/mission_control/jobs/server/workers.rb +13 -0
- data/lib/mission_control/jobs/server.rb +27 -0
- data/lib/mission_control/jobs/tasks.rb +8 -0
- data/lib/mission_control/jobs/version.rb +5 -0
- data/lib/mission_control/jobs/workers_relation.rb +78 -0
- data/lib/mission_control/jobs.rb +43 -0
- data/lib/resque/thread_safe_redis.rb +34 -0
- data/lib/solid_job_monitor.rb +6 -0
- metadata +527 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module MissionControl::Jobs::JobsHelper
|
|
2
|
+
def job_title(job)
|
|
3
|
+
job.job_class_name
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def job_arguments(job)
|
|
7
|
+
renderable_job_arguments_for(job).join(", ")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def failed_job_error(job)
|
|
11
|
+
"#{job.last_execution_error.error_class}: #{job.last_execution_error.message}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def clean_backtrace?
|
|
15
|
+
params["clean_backtrace"] == "true"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def failed_job_backtrace(job, server)
|
|
19
|
+
if clean_backtrace? && server&.backtrace_cleaner
|
|
20
|
+
server.backtrace_cleaner.clean(job.last_execution_error.backtrace).join("\n")
|
|
21
|
+
else
|
|
22
|
+
job.last_execution_error.backtrace.join("\n")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def attribute_names_for_job_status(status)
|
|
27
|
+
case status.to_s
|
|
28
|
+
when "failed" then [ "Error", "" ]
|
|
29
|
+
when "blocked" then [ "Queue", "Blocked by", "" ]
|
|
30
|
+
when "finished" then [ "Queue", "Finished" ]
|
|
31
|
+
when "scheduled" then [ "Queue", "Scheduled", "" ]
|
|
32
|
+
when "in_progress" then [ "Queue", "Run by", "Running for", "Progress", "" ]
|
|
33
|
+
else []
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def job_delayed?(job)
|
|
38
|
+
job.scheduled_at.before?(MissionControl::Jobs.scheduled_job_delay_threshold.ago)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def blocked_job_expiration(job)
|
|
42
|
+
"Expires #{bidirectional_time_distance_in_words_with_title(job.blocked_until)}".html_safe if job.blocked_until
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def job_progress_enabled?
|
|
46
|
+
MissionControl::Jobs::Progress.available?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def progress_for(job)
|
|
50
|
+
@progress_by_job_id&.[](job.job_id) || @progress
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
def renderable_job_arguments_for(job)
|
|
55
|
+
job.serialized_arguments.collect do |argument|
|
|
56
|
+
as_renderable_argument(argument)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def as_renderable_argument(argument)
|
|
61
|
+
case argument
|
|
62
|
+
when Hash
|
|
63
|
+
as_renderable_hash(argument)
|
|
64
|
+
when Array
|
|
65
|
+
as_renderable_array(argument)
|
|
66
|
+
else
|
|
67
|
+
ActiveJob::Arguments.deserialize([ argument ]).first
|
|
68
|
+
end
|
|
69
|
+
rescue ActiveJob::DeserializationError
|
|
70
|
+
argument.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def as_renderable_hash(argument)
|
|
74
|
+
if argument["_aj_globalid"]
|
|
75
|
+
# don't deserialize as the class might not exist in the host app running the engine
|
|
76
|
+
argument["_aj_globalid"]
|
|
77
|
+
elsif argument["_aj_serialized"] == "ActiveJob::Serializers::ModuleSerializer"
|
|
78
|
+
argument["value"]
|
|
79
|
+
elsif argument["_aj_serialized"]
|
|
80
|
+
ActiveJob::Arguments.deserialize([ argument ]).first
|
|
81
|
+
else
|
|
82
|
+
MissionControl::Jobs.job_arguments_filter.apply_to(argument)
|
|
83
|
+
.without("_aj_symbol_keys", "_aj_ruby2_keywords")
|
|
84
|
+
.transform_values { |v| as_renderable_argument(v) }
|
|
85
|
+
.map { |k, v| "#{k}: #{v}" }
|
|
86
|
+
.join(", ")
|
|
87
|
+
.then { |s| "{#{s}}" }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def as_renderable_array(argument)
|
|
92
|
+
"[#{argument.collect { |part| as_renderable_argument(part) }.join(", ")}]"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module MissionControl::Jobs::NavigationHelper
|
|
2
|
+
attr_reader :page_title, :current_section
|
|
3
|
+
|
|
4
|
+
def back_to_main_app_path
|
|
5
|
+
MissionControl::Jobs.back_to_main_app_path.presence || main_app.try(:root_path)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def navigation_sections
|
|
9
|
+
{ queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
|
|
10
|
+
supported_job_statuses.without(:pending).each do |status|
|
|
11
|
+
sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sections[:workers] = [ "Workers", application_workers_path(@application) ] if workers_exposed?
|
|
15
|
+
sections[:recurring_tasks] = [ "Recurring tasks", application_recurring_tasks_path(@application) ] if recurring_tasks_supported?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def navigation_section_for_status(status)
|
|
20
|
+
if status.nil? || status == :pending
|
|
21
|
+
:queues
|
|
22
|
+
else
|
|
23
|
+
"#{status}_jobs".to_sym
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def navigation(title: nil, section: nil)
|
|
28
|
+
@page_title = title
|
|
29
|
+
@current_section = section
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def selected_application?(application)
|
|
33
|
+
MissionControl::Jobs::Current.application.name == application.name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def selectable_applications
|
|
37
|
+
MissionControl::Jobs.applications.reject { |app| selected_application?(app) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def selected_server?(server)
|
|
41
|
+
MissionControl::Jobs::Current.server.name == server.name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def jobs_count_with_status(status)
|
|
45
|
+
count = ActiveJob.jobs.with_status(status).count
|
|
46
|
+
if count.infinite?
|
|
47
|
+
"..."
|
|
48
|
+
else
|
|
49
|
+
number_to_human(count,
|
|
50
|
+
format: "%n%u",
|
|
51
|
+
units: {
|
|
52
|
+
thousand: "K",
|
|
53
|
+
million: "M",
|
|
54
|
+
billion: "B",
|
|
55
|
+
trillion: "T",
|
|
56
|
+
quadrillion: "Q"
|
|
57
|
+
})
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { debounce } from "helpers"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static values = {
|
|
6
|
+
debounceTimeout: { type: Number, default: 300 }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
initialize() {
|
|
10
|
+
this.debouncedSubmit = debounce(this.debouncedSubmit.bind(this), this.debounceTimeoutValue)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
submit(event) {
|
|
14
|
+
const form = event.target.form || event.target.closest("form")
|
|
15
|
+
if (form) form.requestSubmit()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
debouncedSubmit(event) {
|
|
19
|
+
this.submit(event)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Import and register all your controllers from the importmap under controllers/*
|
|
2
|
+
|
|
3
|
+
import { application } from "controllers/application"
|
|
4
|
+
|
|
5
|
+
// Eager load all controllers defined in the import map under controllers/**/*_controller
|
|
6
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
7
|
+
eagerLoadControllersFrom("controllers", application)
|
|
8
|
+
|
|
9
|
+
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
|
|
10
|
+
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
11
|
+
// lazyLoadControllersFrom("controllers", application)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = [ "bar", "label", "stopping" ]
|
|
5
|
+
static values = {
|
|
6
|
+
url: String,
|
|
7
|
+
percent: { type: Number, default: 0 },
|
|
8
|
+
interval: { type: Number, default: 2000 }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.apply({ percent: this.percentValue, stop_requested: this.hasStoppingTarget && !this.stoppingTarget.classList.contains("is-hidden") })
|
|
13
|
+
this.timer = setInterval(() => this.refresh(), this.intervalValue)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
disconnect() {
|
|
17
|
+
if (this.timer) clearInterval(this.timer)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async refresh() {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(this.urlValue, {
|
|
23
|
+
headers: { Accept: "application/json" }
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
this.disconnect()
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const data = await response.json()
|
|
32
|
+
if (data.missing) {
|
|
33
|
+
this.disconnect()
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.apply(data)
|
|
38
|
+
|
|
39
|
+
if (data.percent === 100) this.disconnect()
|
|
40
|
+
} catch (_error) {
|
|
41
|
+
this.disconnect()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
apply(data) {
|
|
46
|
+
const percent = Number.isFinite(data.percent) ? data.percent : 0
|
|
47
|
+
|
|
48
|
+
if (this.hasBarTarget) this.barTarget.style.width = `${percent}%`
|
|
49
|
+
if (this.hasLabelTarget) this.labelTarget.textContent = `${percent}%`
|
|
50
|
+
|
|
51
|
+
if (this.hasStoppingTarget) {
|
|
52
|
+
this.stoppingTarget.classList.toggle("is-hidden", !data.stop_requested)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "helpers/debounce_helpers"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class MissionControl::Jobs::Page
|
|
2
|
+
DEFAULT_PAGE_SIZE = 10
|
|
3
|
+
|
|
4
|
+
attr_reader :records, :index, :page_size
|
|
5
|
+
|
|
6
|
+
def initialize(relation, page: 1, page_size: DEFAULT_PAGE_SIZE)
|
|
7
|
+
@relation = relation
|
|
8
|
+
@page_size = page_size
|
|
9
|
+
@index = [ page, 1 ].max
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def records
|
|
13
|
+
@relation.limit(page_size).offset(offset)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def first?
|
|
17
|
+
index == 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def last?
|
|
21
|
+
index == pages_count || empty? || records.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def empty?
|
|
25
|
+
total_count == 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def previous_index
|
|
29
|
+
[ index - 1, 1 ].max
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def next_index
|
|
33
|
+
pages_count ? [ index + 1, pages_count ].min : index + 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pages_count
|
|
37
|
+
(total_count.to_f / page_size).ceil unless total_count.infinite?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def total_count
|
|
41
|
+
@total_count ||= @relation.count # Potentially expensive when filtering a lot of records, with the adapter in charge of doing the filtering in memory
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
def offset
|
|
46
|
+
(index - 1) * page_size
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class MissionControl::Jobs::RecurringTask
|
|
2
|
+
include ActiveModel::Model
|
|
3
|
+
|
|
4
|
+
attr_accessor :id, :job_class_name, :command, :arguments, :schedule, :last_enqueued_at, :next_time, :queue_name, :priority
|
|
5
|
+
|
|
6
|
+
def initialize(queue_adapter: ActiveJob::Base.queue_adapter, **kwargs)
|
|
7
|
+
@queue_adapter = queue_adapter
|
|
8
|
+
super(**kwargs)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def jobs
|
|
12
|
+
ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).where(recurring_task_id: id)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enqueue
|
|
16
|
+
queue_adapter.enqueue_recurring_task(id)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def runnable?
|
|
20
|
+
queue_adapter.can_enqueue_recurring_task?(id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
attr_reader :queue_adapter
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class MissionControl::Jobs::Worker
|
|
2
|
+
include ActiveModel::Model
|
|
3
|
+
|
|
4
|
+
attr_accessor :id, :name, :hostname, :last_heartbeat_at, :configuration, :raw_data
|
|
5
|
+
|
|
6
|
+
def initialize(queue_adapter: ActiveJob::Base.queue_adapter, **kwargs)
|
|
7
|
+
@queue_adapter = queue_adapter
|
|
8
|
+
super(**kwargs)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def jobs
|
|
12
|
+
@jobs ||= ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).in_progress.where(worker_id: id)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
attr_reader :queue_adapter
|
|
17
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<nav class="navbar" role="navigation" aria-label="main navigation">
|
|
2
|
+
<div class="navbar-menu is-active mb-4">
|
|
3
|
+
<div class="navbar-start">
|
|
4
|
+
<% if (path = back_to_main_app_path) %>
|
|
5
|
+
<%= link_to "Back to main app", path %>
|
|
6
|
+
<% end %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="navbar-end">
|
|
10
|
+
<%= render "layouts/mission_control/jobs/application_selection/servers", application: @application %>
|
|
11
|
+
<%= render "layouts/mission_control/jobs/application_selection/applications" if selectable_applications.any? %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</nav>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Mission control - <%= page_title %></title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
9
|
+
<meta name="turbo-cache-control" content="no-cache">
|
|
10
|
+
|
|
11
|
+
<%= stylesheet_link_tag "mission_control/jobs/bulma.min" %>
|
|
12
|
+
<%= stylesheet_link_tag "mission_control/jobs/application", "data-turbo-track": "reload" %>
|
|
13
|
+
<%= javascript_importmap_tags "application", importmap: MissionControl::Jobs.importmap %>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
|
|
17
|
+
<section class="section">
|
|
18
|
+
<div class="container">
|
|
19
|
+
<%= render "layouts/mission_control/jobs/application_selection" %>
|
|
20
|
+
<%= render "layouts/mission_control/jobs/flash" %>
|
|
21
|
+
<%= render "layouts/mission_control/jobs/navigation" %>
|
|
22
|
+
<%= yield %>
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="application-selector navbar-item has-dropdown is-hoverable">
|
|
2
|
+
<a class="navbar-link">
|
|
3
|
+
<%= MissionControl::Jobs::Current.application.name %>
|
|
4
|
+
</a>
|
|
5
|
+
|
|
6
|
+
<div class="navbar-dropdown">
|
|
7
|
+
<% selectable_applications.each do |application| %>
|
|
8
|
+
<%= link_to application_queues_path(application, server_id: nil), class: "navbar-item" do %>
|
|
9
|
+
<span><%= application.name %></span>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% if application.servers.many? %>
|
|
2
|
+
<div class="server-selector navbar-item">
|
|
3
|
+
<div class="tabs is-toggle is-toggle-rounded is-small">
|
|
4
|
+
<ul>
|
|
5
|
+
<% application.servers.each do |server| %>
|
|
6
|
+
<li class="<%= "is-active" if selected_server?(server) %>">
|
|
7
|
+
<%= link_to application_queues_path(application, server_id: server) do %>
|
|
8
|
+
<span><%= server.name %></span>
|
|
9
|
+
<% end %>
|
|
10
|
+
</li>
|
|
11
|
+
<% end %>
|
|
12
|
+
</ul>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% if job.failed? %>
|
|
2
|
+
<h2 class="subtitle">Error information</h2>
|
|
3
|
+
|
|
4
|
+
<table class="table">
|
|
5
|
+
<tbody>
|
|
6
|
+
<tr>
|
|
7
|
+
<th>Type</th>
|
|
8
|
+
<td> <%= job.last_execution_error.error_class %> </td>
|
|
9
|
+
</tr>
|
|
10
|
+
<tr>
|
|
11
|
+
<th>Message</th>
|
|
12
|
+
<td> <%= job.last_execution_error.try(:message) || job.last_execution_error.inspect %> </td>
|
|
13
|
+
</tr>
|
|
14
|
+
</tbody>
|
|
15
|
+
</table>
|
|
16
|
+
|
|
17
|
+
<% if @server.backtrace_cleaner %>
|
|
18
|
+
<%= render "mission_control/jobs/jobs/failed/backtrace_toggle", application: @application, job: job %>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<pre class="is-family-monospace mb-4 backtrace-content"><%= failed_job_backtrace(job, @server) %></pre>
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div class="filter level-left">
|
|
2
|
+
<%= form_for :filter, url: application_jobs_path(MissionControl::Jobs::Current.application, jobs_status), method: :get,
|
|
3
|
+
data: { controller: "form", action: "input->form#debouncedSubmit" } do |form| %>
|
|
4
|
+
|
|
5
|
+
<div class="field is-grouped">
|
|
6
|
+
<div class="control">
|
|
7
|
+
<%= form.label :job_class_name, class: "label" %>
|
|
8
|
+
<div class="select is-rounded">
|
|
9
|
+
<%= form.text_field :job_class_name, value: @job_filters[:job_class_name], class: "input", list: "job-classes", placeholder: "Filter by job class...", autocomplete: "off" %>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="control">
|
|
14
|
+
<%= form.label :queue_name, class: "label" %>
|
|
15
|
+
<div class="select is-rounded">
|
|
16
|
+
<%= form.text_field :queue_name, value: @job_filters[:queue_name], class: "input", list: "queue-names", placeholder: "Filter by queue name...", autocomplete: "off" %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<% if jobs_status == "finished" %>
|
|
21
|
+
<div class="control">
|
|
22
|
+
<%= form.label :finished_at_start, class: "label" %>
|
|
23
|
+
<div class="select is-rounded">
|
|
24
|
+
<%= form.datetime_field :finished_at_start, value: @job_filters[:finished_at]&.begin, class: "input", placeholder: "Finished from" %>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="control">
|
|
29
|
+
<%= form.label :finished_at_end, class: "label" %>
|
|
30
|
+
<div class="select is-rounded">
|
|
31
|
+
<%= form.datetime_field :finished_at_end, value: @job_filters[:finished_at]&.end, class: "input", placeholder: "Finished to" %>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<%= hidden_field_tag :server_id, MissionControl::Jobs::Current.server.id %>
|
|
37
|
+
|
|
38
|
+
<datalist id="job-classes" class="is-hidden">
|
|
39
|
+
<% job_class_names.each do |job_class_name| %>
|
|
40
|
+
<option value="<%= job_class_name %>"></option>
|
|
41
|
+
<% end %>
|
|
42
|
+
</datalist>
|
|
43
|
+
|
|
44
|
+
<datalist id="queue-names" class="is-hidden">
|
|
45
|
+
<% queue_names.each do |queue_name| %>
|
|
46
|
+
<option value="<%= queue_name %>"></option>
|
|
47
|
+
<% end %>
|
|
48
|
+
</datalist>
|
|
49
|
+
|
|
50
|
+
<div class="control is-align-self-flex-end">
|
|
51
|
+
<%= link_to "Clear", application_jobs_path(MissionControl::Jobs::Current.application, jobs_status, job_class_name: nil, queue_name: nil, finished_at: nil..nil), class: "button" %>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<table class="table">
|
|
2
|
+
<tbody>
|
|
3
|
+
<tr>
|
|
4
|
+
<th>Arguments</th>
|
|
5
|
+
<td>
|
|
6
|
+
<div class="is-family-monospace">
|
|
7
|
+
<%= job_arguments(job) %>
|
|
8
|
+
</div>
|
|
9
|
+
</td>
|
|
10
|
+
</tr>
|
|
11
|
+
<tr>
|
|
12
|
+
<th>Job id</th>
|
|
13
|
+
<td><%= job.job_id %></td>
|
|
14
|
+
</tr>
|
|
15
|
+
<tr>
|
|
16
|
+
<th>Queue</th>
|
|
17
|
+
<td>
|
|
18
|
+
<div class="tag">
|
|
19
|
+
<%= link_to job.queue_name, application_queue_path(@application, job.queue) %>
|
|
20
|
+
</div>
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
23
|
+
<tr>
|
|
24
|
+
<th>Enqueued</th>
|
|
25
|
+
<td>
|
|
26
|
+
<%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
29
|
+
<% if job.executions > 0 %>
|
|
30
|
+
<tr>
|
|
31
|
+
<th>Retries</th>
|
|
32
|
+
<td><%= job.executions %></td>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% if job.scheduled? %>
|
|
36
|
+
<tr>
|
|
37
|
+
<th>Scheduled</th>
|
|
38
|
+
<td>
|
|
39
|
+
<%= bidirectional_time_distance_in_words_with_title(job.scheduled_at) %>
|
|
40
|
+
<% if job_delayed?(job) %>
|
|
41
|
+
<div class="is-danger tag ml-4">delayed</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<% end %>
|
|
46
|
+
<% if job.failed? %>
|
|
47
|
+
<tr>
|
|
48
|
+
<th>Failed</th>
|
|
49
|
+
<td>
|
|
50
|
+
<%= time_distance_in_words_with_title(job.failed_at) %> ago
|
|
51
|
+
</td>
|
|
52
|
+
</tr>
|
|
53
|
+
<% end %>
|
|
54
|
+
<% if job.finished_at.present? %>
|
|
55
|
+
<tr>
|
|
56
|
+
<th>Finished</th>
|
|
57
|
+
<td>
|
|
58
|
+
<%= time_distance_in_words_with_title(job.finished_at) %> ago
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
<tr>
|
|
62
|
+
<th>Duration</th>
|
|
63
|
+
<td>
|
|
64
|
+
<%= job.duration.round(3) %> seconds
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% if job.worker_id.present? %>
|
|
69
|
+
<tr>
|
|
70
|
+
<th>Processed by</th>
|
|
71
|
+
<td>
|
|
72
|
+
<%= link_to "worker #{job.worker_id}", application_worker_path(@application, job.worker_id) %>
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
<% end %>
|
|
76
|
+
<% if job.in_progress? || progress_for(job) %>
|
|
77
|
+
<tr>
|
|
78
|
+
<th>Progress</th>
|
|
79
|
+
<td>
|
|
80
|
+
<%= render "mission_control/jobs/jobs/progress_bar", progress: progress_for(job), job: job %>
|
|
81
|
+
</td>
|
|
82
|
+
</tr>
|
|
83
|
+
<% end %>
|
|
84
|
+
</tbody>
|
|
85
|
+
</table>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<tr class="job">
|
|
2
|
+
<td>
|
|
3
|
+
<%= link_to job_title(job), application_job_path(@application, job.job_id) %>
|
|
4
|
+
|
|
5
|
+
<% if job.serialized_arguments.present? %>
|
|
6
|
+
<div class="is-family-monospace"><%= job_arguments(job) %></div>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<div class="has-text-grey is-size-7">Enqueued <%= time_distance_in_words_with_title(job.enqueued_at.to_datetime) %> ago</div>
|
|
10
|
+
</td>
|
|
11
|
+
|
|
12
|
+
<%= render "mission_control/jobs/jobs/#{jobs_status}/job", job: job %>
|
|
13
|
+
</tr>
|