flight_control 2.0.0.beta.1 → 2.0.0.beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -1
- data/README.md +23 -0
- data/app/assets/config/flight_control_manifest.js +3 -0
- data/app/assets/stylesheets/flight_control/application.css +5 -0
- data/app/assets/stylesheets/flight_control/bulma.min.css +21013 -0
- data/app/assets/stylesheets/flight_control/forms.css +8 -0
- data/app/assets/stylesheets/flight_control/jobs.css +42 -0
- data/app/controllers/concerns/flight_control/adapter_features.rb +25 -0
- data/app/controllers/concerns/flight_control/application_scoped.rb +39 -0
- data/app/controllers/concerns/flight_control/basic_authentication.rb +34 -0
- data/app/controllers/concerns/flight_control/failed_jobs_bulk_operations.rb +18 -0
- data/app/controllers/concerns/flight_control/job_filters.rb +44 -0
- data/app/controllers/concerns/flight_control/job_scoped.rb +17 -0
- data/app/controllers/concerns/flight_control/not_found_redirections.rb +36 -0
- data/app/controllers/concerns/flight_control/queue_scoped.rb +13 -0
- data/app/controllers/flight_control/application_controller.rb +35 -0
- data/app/controllers/flight_control/bulk_discards_controller.rb +21 -0
- data/app/controllers/flight_control/bulk_retries_controller.rb +10 -0
- data/app/controllers/flight_control/discards_controller.rb +19 -0
- data/app/controllers/flight_control/dispatches_controller.rb +19 -0
- data/app/controllers/flight_control/job_launches_controller.rb +28 -0
- data/app/controllers/flight_control/jobs_controller.rb +40 -0
- data/app/controllers/flight_control/queues/pauses_controller.rb +15 -0
- data/app/controllers/flight_control/queues_controller.rb +17 -0
- data/app/controllers/flight_control/recurring_tasks_controller.rb +39 -0
- data/app/controllers/flight_control/retries_controller.rb +14 -0
- data/app/controllers/flight_control/workers_controller.rb +24 -0
- data/app/helpers/flight_control/application_helper.rb +12 -0
- data/app/helpers/flight_control/dates_helper.rb +30 -0
- data/app/helpers/flight_control/interface_helper.rb +23 -0
- data/app/helpers/flight_control/jobs_helper.rb +105 -0
- data/app/helpers/flight_control/navigation_helper.rb +61 -0
- data/app/javascript/flight_control/application.js +3 -0
- data/app/javascript/flight_control/controllers/application.js +9 -0
- data/app/javascript/flight_control/controllers/form_controller.js +8 -0
- data/app/javascript/flight_control/controllers/index.js +11 -0
- data/app/javascript/flight_control/controllers/local_time_controller.js +27 -0
- data/app/models/flight_control/application_record.rb +5 -0
- data/app/models/flight_control/current.rb +3 -0
- data/app/models/flight_control/job_launch.rb +75 -0
- data/app/models/flight_control/page.rb +49 -0
- data/app/models/flight_control/recurring_task.rb +26 -0
- data/app/models/flight_control/worker.rb +18 -0
- data/app/views/flight_control/job_launches/new.html.erb +53 -0
- data/app/views/flight_control/jobs/_date_range_filter.html.erb +11 -0
- data/app/views/flight_control/jobs/_error_information.html.erb +24 -0
- data/app/views/flight_control/jobs/_filters.html.erb +49 -0
- data/app/views/flight_control/jobs/_general_information.html.erb +77 -0
- data/app/views/flight_control/jobs/_job.html.erb +13 -0
- data/app/views/flight_control/jobs/_jobs_page.html.erb +13 -0
- data/app/views/flight_control/jobs/_raw_data.html.erb +4 -0
- data/app/views/flight_control/jobs/_title.html.erb +19 -0
- data/app/views/flight_control/jobs/_toolbar.html.erb +18 -0
- data/app/views/flight_control/jobs/blocked/_actions.html.erb +3 -0
- data/app/views/flight_control/jobs/blocked/_job.html.erb +8 -0
- data/app/views/flight_control/jobs/failed/_actions.html.erb +5 -0
- data/app/views/flight_control/jobs/failed/_backtrace_toggle.html.erb +14 -0
- data/app/views/flight_control/jobs/failed/_job.html.erb +8 -0
- data/app/views/flight_control/jobs/finished/_job.html.erb +3 -0
- data/app/views/flight_control/jobs/in_progress/_job.html.erb +9 -0
- data/app/views/flight_control/jobs/index.html.erb +19 -0
- data/app/views/flight_control/jobs/scheduled/_actions.html.erb +5 -0
- data/app/views/flight_control/jobs/scheduled/_job.html.erb +13 -0
- data/app/views/flight_control/jobs/show.html.erb +6 -0
- data/app/views/flight_control/queues/_actions.html.erb +7 -0
- data/app/views/flight_control/queues/_job.html.erb +15 -0
- data/app/views/flight_control/queues/_queue.html.erb +16 -0
- data/app/views/flight_control/queues/_queue_title.html.erb +17 -0
- data/app/views/flight_control/queues/index.html.erb +21 -0
- data/app/views/flight_control/queues/show.html.erb +25 -0
- data/app/views/flight_control/recurring_tasks/_actions.html.erb +5 -0
- data/app/views/flight_control/recurring_tasks/_general_information.html.erb +37 -0
- data/app/views/flight_control/recurring_tasks/_recurring_task.html.erb +22 -0
- data/app/views/flight_control/recurring_tasks/_title.html.erb +10 -0
- data/app/views/flight_control/recurring_tasks/index.html.erb +18 -0
- data/app/views/flight_control/recurring_tasks/show.html.erb +14 -0
- data/app/views/flight_control/shared/_job.html.erb +27 -0
- data/app/views/flight_control/shared/_jobs.html.erb +14 -0
- data/app/views/flight_control/shared/_pagination_toolbar.html.erb +7 -0
- data/app/views/flight_control/workers/_configuration.html.erb +6 -0
- data/app/views/flight_control/workers/_raw_data.html.erb +6 -0
- data/app/views/flight_control/workers/_title.html.erb +11 -0
- data/app/views/flight_control/workers/_worker.html.erb +21 -0
- data/app/views/flight_control/workers/_workers_page.html.erb +15 -0
- data/app/views/flight_control/workers/index.html.erb +6 -0
- data/app/views/flight_control/workers/show.html.erb +14 -0
- data/app/views/layouts/flight_control/_application_selection.html.erb +14 -0
- data/app/views/layouts/flight_control/_flash.html.erb +9 -0
- data/app/views/layouts/flight_control/_navigation.html.erb +9 -0
- data/app/views/layouts/flight_control/application.html.erb +28 -0
- data/app/views/layouts/flight_control/application_selection/_applications.html.erb +13 -0
- data/app/views/layouts/flight_control/application_selection/_servers.html.erb +15 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +34 -0
- data/lib/active_job/job_proxy.rb +2 -1
- data/lib/active_job/jobs_relation.rb +17 -5
- data/lib/active_job/queue_adapters/solid_queue_ext.rb +43 -5
- data/lib/flight_control/engine.rb +7 -1
- data/lib/flight_control/host_route_helpers.rb +34 -0
- data/lib/flight_control/version.rb +1 -1
- data/lib/flight_control.rb +1 -0
- metadata +94 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module FlightControl::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 job_duration(job)
|
|
27
|
+
return unless (seconds = job.duration)
|
|
28
|
+
tag.span formatted_duration(seconds), title: "#{seconds.round(3)} seconds"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def attribute_names_for_job_status(status)
|
|
32
|
+
case status.to_s
|
|
33
|
+
when "failed" then ["Error", "Duration", ""]
|
|
34
|
+
when "blocked" then ["Queue", "Blocked by", ""]
|
|
35
|
+
when "finished" then ["Queue", "Duration", "Finished"]
|
|
36
|
+
when "scheduled" then ["Queue", "Scheduled", ""]
|
|
37
|
+
when "in_progress" then ["Queue", "Run by", "Running for"]
|
|
38
|
+
else []
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def job_delayed?(job)
|
|
43
|
+
job.scheduled_at.before?(FlightControl.scheduled_job_delay_threshold.ago)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def blocked_job_expiration(job)
|
|
47
|
+
"Expires #{bidirectional_time_distance_in_words_with_title(job.blocked_until)}".html_safe if job.blocked_until
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
DURATION_UNIT_ABBREVIATIONS = {years: "y", months: "mo", weeks: "w", days: "d", hours: "h", minutes: "m", seconds: "s"}
|
|
53
|
+
|
|
54
|
+
def formatted_duration(seconds)
|
|
55
|
+
if seconds < 1
|
|
56
|
+
"#{(seconds * 1000).round} ms"
|
|
57
|
+
elsif seconds < 60
|
|
58
|
+
"#{seconds.round(1)} s"
|
|
59
|
+
else
|
|
60
|
+
ActiveSupport::Duration.build(seconds.round).parts
|
|
61
|
+
.map { |unit, value| "#{value}#{DURATION_UNIT_ABBREVIATIONS[unit]}" }.join(" ")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def renderable_job_arguments_for(job)
|
|
66
|
+
job.serialized_arguments.collect do |argument|
|
|
67
|
+
as_renderable_argument(argument)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def as_renderable_argument(argument)
|
|
72
|
+
case argument
|
|
73
|
+
when Hash
|
|
74
|
+
as_renderable_hash(argument)
|
|
75
|
+
when Array
|
|
76
|
+
as_renderable_array(argument)
|
|
77
|
+
else
|
|
78
|
+
ActiveJob::Arguments.deserialize([argument]).first
|
|
79
|
+
end
|
|
80
|
+
rescue ActiveJob::DeserializationError
|
|
81
|
+
argument.to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def as_renderable_hash(argument)
|
|
85
|
+
if argument["_aj_globalid"]
|
|
86
|
+
# don't deserialize as the class might not exist in the host app running the engine
|
|
87
|
+
argument["_aj_globalid"]
|
|
88
|
+
elsif argument["_aj_serialized"] == "ActiveJob::Serializers::ModuleSerializer"
|
|
89
|
+
argument["value"]
|
|
90
|
+
elsif argument["_aj_serialized"]
|
|
91
|
+
ActiveJob::Arguments.deserialize([argument]).first
|
|
92
|
+
else
|
|
93
|
+
FlightControl.job_arguments_filter.apply_to(argument)
|
|
94
|
+
.without("_aj_symbol_keys", "_aj_ruby2_keywords")
|
|
95
|
+
.transform_values { |v| as_renderable_argument(v) }
|
|
96
|
+
.map { |k, v| "#{k}: #{v}" }
|
|
97
|
+
.join(", ")
|
|
98
|
+
.then { |s| "{#{s}}" }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def as_renderable_array(argument)
|
|
103
|
+
"[#{argument.collect { |part| as_renderable_argument(part) }.join(", ")}]"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module FlightControl::NavigationHelper
|
|
2
|
+
attr_reader :page_title, :current_section
|
|
3
|
+
|
|
4
|
+
def back_to_main_app_path
|
|
5
|
+
FlightControl.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
|
+
sections[:job_launches] = ["Run job", new_application_job_launch_path(@application)]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def navigation_section_for_status(status)
|
|
21
|
+
if status.nil? || status == :pending
|
|
22
|
+
:queues
|
|
23
|
+
else
|
|
24
|
+
:"#{status}_jobs"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def navigation(title: nil, section: nil)
|
|
29
|
+
@page_title = title
|
|
30
|
+
@current_section = section
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def selected_application?(application)
|
|
34
|
+
FlightControl::Current.application.name == application.name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def selectable_applications
|
|
38
|
+
FlightControl.applications.reject { |app| selected_application?(app) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def selected_server?(server)
|
|
42
|
+
FlightControl::Current.server.name == server.name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def jobs_count_with_status(status)
|
|
46
|
+
count = ActiveJob.jobs.with_status(status).count
|
|
47
|
+
if count.infinite?
|
|
48
|
+
"..."
|
|
49
|
+
else
|
|
50
|
+
number_to_human(count,
|
|
51
|
+
format: "%n%u",
|
|
52
|
+
units: {
|
|
53
|
+
thousand: "K",
|
|
54
|
+
million: "M",
|
|
55
|
+
billion: "B",
|
|
56
|
+
trillion: "T",
|
|
57
|
+
quadrillion: "Q"
|
|
58
|
+
})
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -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,27 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Replaces a server-rendered UTC timestamp with "<browser-local time> · <UTC time>".
|
|
4
|
+
// Leaves the UTC-only fallback in place when the browser's timezone is UTC.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static values = { datetime: String }
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
const date = new Date(this.datetimeValue)
|
|
10
|
+
if (isNaN(date)) return
|
|
11
|
+
|
|
12
|
+
const local = this.format(date)
|
|
13
|
+
const utc = this.format(date, "UTC")
|
|
14
|
+
this.element.textContent = local === utc ? utc : `${local} · ${utc}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
format(date, timeZone = undefined) {
|
|
18
|
+
// en-CA yields "2026-07-10, 12:32:15 UTC"
|
|
19
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
20
|
+
timeZone,
|
|
21
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
22
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
23
|
+
hourCycle: "h23",
|
|
24
|
+
timeZoneName: "short"
|
|
25
|
+
}).format(date).replace(",", "")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
class FlightControl::JobLaunch
|
|
2
|
+
include ActiveModel::Model
|
|
3
|
+
include ActiveModel::Attributes
|
|
4
|
+
|
|
5
|
+
attribute :job_class_name, :string
|
|
6
|
+
attribute :arguments_json, :string, default: "[]"
|
|
7
|
+
attribute :queue_name, :string
|
|
8
|
+
|
|
9
|
+
validates :job_class_name, presence: true
|
|
10
|
+
validate :validate_job_class
|
|
11
|
+
validate :validate_arguments
|
|
12
|
+
|
|
13
|
+
# Job classes defined in the host application. Not memoized on purpose:
|
|
14
|
+
# class reloading in development would leave stale classes behind.
|
|
15
|
+
def self.job_classes
|
|
16
|
+
Rails.application.eager_load! unless Rails.application.config.eager_load
|
|
17
|
+
ActiveJob::Base.descendants.select { |job_class| job_class.name.present? && implements_perform?(job_class) }.sort_by(&:name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# ActiveJob::Execution defines a perform placeholder that raises NotImplementedError,
|
|
21
|
+
# so abstract base classes like ApplicationJob still own that placeholder.
|
|
22
|
+
def self.implements_perform?(job_class)
|
|
23
|
+
job_class.instance_method(:perform).owner != ActiveJob::Execution
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def enqueue
|
|
27
|
+
return nil unless valid?
|
|
28
|
+
|
|
29
|
+
job = configured_job_class.perform_later(*positional_arguments, **keyword_arguments)
|
|
30
|
+
if job&.successfully_enqueued?
|
|
31
|
+
job
|
|
32
|
+
else
|
|
33
|
+
errors.add(:base, "job could not be enqueued#{": #{job.enqueue_error.message}" if job&.enqueue_error}")
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
rescue => error
|
|
37
|
+
errors.add(:base, "job could not be enqueued: #{error.message}")
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def job_class
|
|
44
|
+
# Only classes from the discovered list can be launched; never constantize user input.
|
|
45
|
+
self.class.job_classes.detect { |klass| klass.name == job_class_name }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configured_job_class
|
|
49
|
+
queue_name.present? ? job_class.set(queue: queue_name) : job_class
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parsed_arguments
|
|
53
|
+
@parsed_arguments ||= JSON.parse(arguments_json.presence || "[]")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def positional_arguments
|
|
57
|
+
parsed_arguments.last.is_a?(Hash) ? parsed_arguments[0...-1] : parsed_arguments
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def keyword_arguments
|
|
61
|
+
parsed_arguments.last.is_a?(Hash) ? parsed_arguments.last.transform_keys(&:to_sym) : {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_job_class
|
|
65
|
+
if job_class_name.present? && job_class.nil?
|
|
66
|
+
errors.add(:job_class_name, "is not a job class of this application")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_arguments
|
|
71
|
+
errors.add(:arguments_json, "must be a JSON array") unless parsed_arguments.is_a?(Array)
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
errors.add(:arguments_json, "is not valid JSON")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class FlightControl::Page
|
|
2
|
+
DEFAULT_PAGE_SIZE = 10
|
|
3
|
+
|
|
4
|
+
attr_reader :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
|
+
|
|
46
|
+
def offset
|
|
47
|
+
(index - 1) * page_size
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class FlightControl::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
|
+
|
|
25
|
+
attr_reader :queue_adapter
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class FlightControl::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
|
+
|
|
17
|
+
attr_reader :queue_adapter
|
|
18
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<% navigation(title: "Run job", section: :job_launches) %>
|
|
2
|
+
|
|
3
|
+
<%= form_with model: @job_launch, url: application_job_launches_path(@application) do |form| %>
|
|
4
|
+
<% if @job_launch.errors.any? %>
|
|
5
|
+
<article class="message is-danger">
|
|
6
|
+
<div class="message-body">
|
|
7
|
+
<ul>
|
|
8
|
+
<% @job_launch.errors.full_messages.each do |message| %>
|
|
9
|
+
<li><%= message %></li>
|
|
10
|
+
<% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
</article>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<div class="field">
|
|
17
|
+
<%= form.label :job_class_name, "Job class", class: "label" %>
|
|
18
|
+
<div class="control">
|
|
19
|
+
<div class="select">
|
|
20
|
+
<%= form.select :job_class_name, @job_classes.map(&:name), include_blank: "Select a job class..." %>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="field">
|
|
26
|
+
<%= form.label :arguments_json, "Arguments", class: "label" %>
|
|
27
|
+
<div class="control">
|
|
28
|
+
<%= form.text_area :arguments_json, class: "textarea is-family-monospace", rows: 4, placeholder: '[ 123, "abc", { "some_option": true } ]' %>
|
|
29
|
+
</div>
|
|
30
|
+
<p class="help">
|
|
31
|
+
A JSON array with the job's arguments. A trailing JSON object is passed as keyword arguments.
|
|
32
|
+
Leave <code>[]</code> to run the job without arguments.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="field">
|
|
37
|
+
<%= form.label :queue_name, "Queue (optional)", class: "label" %>
|
|
38
|
+
<div class="control">
|
|
39
|
+
<%= form.text_field :queue_name, class: "input", list: "queue-names", placeholder: "Job class default", autocomplete: "off" %>
|
|
40
|
+
</div>
|
|
41
|
+
<datalist id="queue-names" class="is-hidden">
|
|
42
|
+
<% @queue_names.each do |queue_name| %>
|
|
43
|
+
<option value="<%= queue_name %>"></option>
|
|
44
|
+
<% end %>
|
|
45
|
+
</datalist>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="field">
|
|
49
|
+
<div class="control">
|
|
50
|
+
<%= form.submit "Enqueue job", class: "button is-primary" %>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# locals: (form:, attribute:, label:) %>
|
|
2
|
+
|
|
3
|
+
<div class="control">
|
|
4
|
+
<%= form.label :"#{attribute}_start", "#{label} from", class: "label" %>
|
|
5
|
+
<%= form.datetime_field :"#{attribute}_start", value: @job_filters[attribute]&.begin, class: "input is-rounded" %>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="control">
|
|
9
|
+
<%= form.label :"#{attribute}_end", "#{label} to", class: "label" %>
|
|
10
|
+
<%= form.datetime_field :"#{attribute}_end", value: @job_filters[attribute]&.end, class: "input is-rounded" %>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
<%= turbo_frame_tag "job-backtrace" do %>
|
|
18
|
+
<% if @server.backtrace_cleaner %>
|
|
19
|
+
<%= render "flight_control/jobs/failed/backtrace_toggle", application: @application, job: job %>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<pre class="is-family-monospace mb-4 backtrace-content"><%= failed_job_backtrace(job, @server) %></pre>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div class="filter level-left">
|
|
2
|
+
<%= form_for :filter, url: application_jobs_path(FlightControl::Current.application, jobs_status), method: :get,
|
|
3
|
+
data: { controller: "form", action: "change->form#submit" } do |form| %>
|
|
4
|
+
|
|
5
|
+
<div class="field is-grouped is-grouped-multiline">
|
|
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
|
+
<%= render "flight_control/jobs/date_range_filter", form: form, attribute: :finished_at, label: "Finished" %>
|
|
22
|
+
<% else %>
|
|
23
|
+
<%= render "flight_control/jobs/date_range_filter", form: form, attribute: :enqueued_at, label: "Enqueued" %>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if jobs_status == "scheduled" %>
|
|
27
|
+
<%= render "flight_control/jobs/date_range_filter", form: form, attribute: :scheduled_at, label: "Scheduled" %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<%= hidden_field_tag :server_id, FlightControl::Current.server.id %>
|
|
31
|
+
|
|
32
|
+
<datalist id="job-classes" class="is-hidden">
|
|
33
|
+
<% job_class_names.each do |job_class_name| %>
|
|
34
|
+
<option value="<%= job_class_name %>"></option>
|
|
35
|
+
<% end %>
|
|
36
|
+
</datalist>
|
|
37
|
+
|
|
38
|
+
<datalist id="queue-names" class="is-hidden">
|
|
39
|
+
<% queue_names.each do |queue_name| %>
|
|
40
|
+
<option value="<%= queue_name %>"></option>
|
|
41
|
+
<% end %>
|
|
42
|
+
</datalist>
|
|
43
|
+
|
|
44
|
+
<div class="control is-align-self-flex-end">
|
|
45
|
+
<%= link_to "Clear", application_jobs_path(FlightControl::Current.application, jobs_status), class: "button" %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
<%= timestamp_with_relative_time(job.enqueued_at.to_datetime) %>
|
|
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
|
+
<%= timestamp_with_relative_time(job.finished_at) %>
|
|
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
|
+
</tbody>
|
|
77
|
+
</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"><%= truncate(job_arguments(job), length: 300) %></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 "flight_control/jobs/#{jobs_status}/job", job: job %>
|
|
13
|
+
</tr>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<table class="jobs <%= jobs_status %> table queues is-hoverable is-fullwidth">
|
|
2
|
+
<thead>
|
|
3
|
+
<tr>
|
|
4
|
+
<th class="job-header">Job</th>
|
|
5
|
+
<% attribute_names_for_job_status(jobs_status).each do |attribute| %>
|
|
6
|
+
<th><%= attribute %></th>
|
|
7
|
+
<% end %>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
<%= render partial: "flight_control/jobs/job", collection: jobs_page.records %>
|
|
12
|
+
</tbody>
|
|
13
|
+
</table>
|