mission_control-jobs 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +244 -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/forms.css +8 -0
- data/app/assets/stylesheets/mission_control/jobs/jobs.css +7 -0
- data/app/controllers/concerns/mission_control/jobs/adapter_features.rb +20 -0
- data/app/controllers/concerns/mission_control/jobs/application_scoped.rb +38 -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 +18 -0
- data/app/controllers/concerns/mission_control/jobs/job_scoped.rb +16 -0
- data/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb +25 -0
- data/app/controllers/concerns/mission_control/jobs/queue_scoped.rb +12 -0
- data/app/controllers/mission_control/jobs/application_controller.rb +11 -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 +13 -0
- data/app/controllers/mission_control/jobs/jobs_controller.rb +37 -0
- data/app/controllers/mission_control/jobs/queues/pauses_controller.rb +15 -0
- data/app/controllers/mission_control/jobs/queues_controller.rb +24 -0
- data/app/controllers/mission_control/jobs/retries_controller.rb +13 -0
- data/app/controllers/mission_control/jobs/workers_controller.rb +18 -0
- data/app/helpers/mission_control/jobs/application_helper.rb +8 -0
- data/app/helpers/mission_control/jobs/dates_helper.rb +19 -0
- data/app/helpers/mission_control/jobs/jobs_helper.rb +63 -0
- data/app/helpers/mission_control/jobs/navigation_helper.rb +51 -0
- data/app/helpers/mission_control/jobs/ui_helper.rb +23 -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/helpers/debounce_helpers.js +9 -0
- data/app/javascript/mission_control/jobs/helpers/index.js +1 -0
- data/app/jobs/mission_control/jobs/application_job.rb +6 -0
- data/app/mailers/mission_control/jobs/application_mailer.rb +8 -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/page.rb +48 -0
- data/app/models/mission_control/jobs/worker.rb +17 -0
- data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +11 -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 +25 -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 +19 -0
- data/app/views/mission_control/jobs/jobs/_filters.html.erb +35 -0
- data/app/views/mission_control/jobs/jobs/_general_information.html.erb +54 -0
- data/app/views/mission_control/jobs/jobs/_job.html.erb +13 -0
- data/app/views/mission_control/jobs/jobs/_jobs_page.html.erb +15 -0
- data/app/views/mission_control/jobs/jobs/_raw_data.html.erb +4 -0
- data/app/views/mission_control/jobs/jobs/_title.html.erb +13 -0
- data/app/views/mission_control/jobs/jobs/_toolbar.html.erb +18 -0
- data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +3 -0
- data/app/views/mission_control/jobs/jobs/failed/_actions.html.erb +5 -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/_job.html.erb +9 -0
- data/app/views/mission_control/jobs/jobs/index.html.erb +19 -0
- data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +7 -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/shared/_pagination_toolbar.html.erb +5 -0
- data/app/views/mission_control/jobs/workers/_configuration.html.erb +6 -0
- data/app/views/mission_control/jobs/workers/_job.html.erb +19 -0
- data/app/views/mission_control/jobs/workers/_jobs.html.erb +20 -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/index.html.erb +17 -0
- data/app/views/mission_control/jobs/workers/show.html.erb +7 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +33 -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 +43 -0
- data/lib/active_job/execution_error.rb +8 -0
- data/lib/active_job/failed.rb +11 -0
- data/lib/active_job/job_proxy.rb +26 -0
- data/lib/active_job/jobs_relation.rb +300 -0
- data/lib/active_job/querying.rb +44 -0
- data/lib/active_job/queue.rb +62 -0
- data/lib/active_job/queue_adapters/resque_ext.rb +300 -0
- data/lib/active_job/queue_adapters/solid_queue_ext.rb +294 -0
- data/lib/active_job/queues.rb +29 -0
- data/lib/mission_control/jobs/adapter.rb +108 -0
- data/lib/mission_control/jobs/application.rb +17 -0
- data/lib/mission_control/jobs/applications.rb +8 -0
- data/lib/mission_control/jobs/console/context.rb +11 -0
- data/lib/mission_control/jobs/console/helpers.rb +26 -0
- data/lib/mission_control/jobs/engine.rb +88 -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/identified_by_name.rb +18 -0
- data/lib/mission_control/jobs/identified_elements.rb +23 -0
- data/lib/mission_control/jobs/server/serializable.rb +24 -0
- data/lib/mission_control/jobs/server/workers.rb +15 -0
- data/lib/mission_control/jobs/server.rb +26 -0
- data/lib/mission_control/jobs/version.rb +5 -0
- data/lib/mission_control/jobs.rb +19 -0
- data/lib/resque/thread_safe_redis.rb +34 -0
- data/lib/tasks/mission_control/jobs_tasks.rake +4 -0
- metadata +364 -0
@@ -0,0 +1,294 @@
|
|
1
|
+
module ActiveJob::QueueAdapters::SolidQueueExt
|
2
|
+
include MissionControl::Jobs::Adapter
|
3
|
+
|
4
|
+
def queues
|
5
|
+
queues = SolidQueue::Queue.all
|
6
|
+
pauses = SolidQueue::Pause.where(queue_name: queues.map(&:name)).index_by(&:queue_name)
|
7
|
+
|
8
|
+
queues.collect do |queue|
|
9
|
+
{
|
10
|
+
name: queue.name,
|
11
|
+
size: queue.size,
|
12
|
+
active: pauses[queue.name].nil?
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def queue_size(queue_name)
|
18
|
+
find_queue_by_name(queue_name).size
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear_queue(queue_name)
|
22
|
+
find_queue_by_name(queue_name).clear
|
23
|
+
end
|
24
|
+
|
25
|
+
def pause_queue(queue_name)
|
26
|
+
find_queue_by_name(queue_name).pause
|
27
|
+
end
|
28
|
+
|
29
|
+
def resume_queue(queue_name)
|
30
|
+
find_queue_by_name(queue_name).resume
|
31
|
+
end
|
32
|
+
|
33
|
+
def queue_paused?(queue_name)
|
34
|
+
find_queue_by_name(queue_name).paused?
|
35
|
+
end
|
36
|
+
|
37
|
+
def supported_statuses
|
38
|
+
RelationAdapter::STATUS_MAP.keys
|
39
|
+
end
|
40
|
+
|
41
|
+
def supported_filters(*)
|
42
|
+
[ :queue_name, :job_class_name ]
|
43
|
+
end
|
44
|
+
|
45
|
+
def exposes_workers?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def workers
|
50
|
+
SolidQueue::Process.where(kind: "Worker").collect do |process|
|
51
|
+
worker_attributes_from_solid_queue_process(process)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_worker(worker_id)
|
56
|
+
if process = SolidQueue::Process.find_by(id: worker_id)
|
57
|
+
worker_attributes_from_solid_queue_process(process)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def jobs_count(jobs_relation)
|
62
|
+
RelationAdapter.new(jobs_relation).count
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_jobs(jobs_relation)
|
66
|
+
find_solid_queue_jobs_within(jobs_relation).map { |job| deserialize_and_proxy_solid_queue_job(job, jobs_relation.status) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def retry_all_jobs(jobs_relation)
|
70
|
+
RelationAdapter.new(jobs_relation).retry_all
|
71
|
+
end
|
72
|
+
|
73
|
+
def retry_job(job, jobs_relation)
|
74
|
+
find_solid_queue_job!(job.job_id, jobs_relation).retry
|
75
|
+
end
|
76
|
+
|
77
|
+
def discard_all_jobs(jobs_relation)
|
78
|
+
RelationAdapter.new(jobs_relation).discard_all
|
79
|
+
end
|
80
|
+
|
81
|
+
def discard_job(job, jobs_relation)
|
82
|
+
find_solid_queue_job!(job.job_id, jobs_relation).discard
|
83
|
+
end
|
84
|
+
|
85
|
+
def find_job(job_id, *)
|
86
|
+
if job = SolidQueue::Job.where(active_job_id: job_id).order(:id).last
|
87
|
+
deserialize_and_proxy_solid_queue_job job
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
def find_queue_by_name(queue_name)
|
93
|
+
SolidQueue::Queue.find_by_name(queue_name)
|
94
|
+
end
|
95
|
+
|
96
|
+
def worker_attributes_from_solid_queue_process(process)
|
97
|
+
{
|
98
|
+
id: process.id,
|
99
|
+
name: "PID: #{process.pid}",
|
100
|
+
hostname: process.hostname,
|
101
|
+
last_heartbeat_at: process.last_heartbeat_at,
|
102
|
+
configuration: process.metadata,
|
103
|
+
raw_data: process.as_json
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def find_solid_queue_job!(job_id, jobs_relation)
|
108
|
+
find_solid_queue_job(job_id, jobs_relation) or raise ActiveJob::Errors::JobNotFoundError.new(job_id, jobs_relation)
|
109
|
+
end
|
110
|
+
|
111
|
+
def find_solid_queue_job(job_id, jobs_relation)
|
112
|
+
RelationAdapter.new(jobs_relation).find_job(job_id)
|
113
|
+
end
|
114
|
+
|
115
|
+
def find_solid_queue_jobs_within(jobs_relation)
|
116
|
+
RelationAdapter.new(jobs_relation).jobs
|
117
|
+
end
|
118
|
+
|
119
|
+
def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
|
120
|
+
job_status ||= status_from_solid_queue_job(solid_queue_job)
|
121
|
+
|
122
|
+
ActiveJob::JobProxy.new(solid_queue_job.arguments).tap do |job|
|
123
|
+
job.status = job_status
|
124
|
+
job.last_execution_error = execution_error_from_solid_queue_job(solid_queue_job) if job_status == :failed
|
125
|
+
job.raw_data = solid_queue_job.as_json
|
126
|
+
job.failed_at = solid_queue_job&.failed_execution&.created_at if job_status == :failed
|
127
|
+
job.finished_at = solid_queue_job.finished_at
|
128
|
+
job.blocked_by = solid_queue_job.concurrency_key
|
129
|
+
job.blocked_until = solid_queue_job&.blocked_execution&.expires_at if job_status == :blocked
|
130
|
+
job.worker_id = solid_queue_job&.claimed_execution&.process_id if job_status == :in_progress
|
131
|
+
job.started_at = solid_queue_job&.claimed_execution&.created_at if job_status == :in_progress
|
132
|
+
job.scheduled_at = solid_queue_job.scheduled_at
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def status_from_solid_queue_job(solid_queue_job)
|
137
|
+
RelationAdapter::STATUS_MAP.invert[solid_queue_job.status]
|
138
|
+
end
|
139
|
+
|
140
|
+
def execution_error_from_solid_queue_job(solid_queue_job)
|
141
|
+
if solid_queue_job.failed?
|
142
|
+
ActiveJob::ExecutionError.new \
|
143
|
+
error_class: solid_queue_job.failed_execution.exception_class,
|
144
|
+
message: solid_queue_job.failed_execution.message,
|
145
|
+
backtrace: solid_queue_job.failed_execution.backtrace
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class RelationAdapter
|
150
|
+
STATUS_MAP = {
|
151
|
+
pending: :ready,
|
152
|
+
failed: :failed,
|
153
|
+
in_progress: :claimed,
|
154
|
+
blocked: :blocked,
|
155
|
+
scheduled: :scheduled,
|
156
|
+
finished: :finished
|
157
|
+
}
|
158
|
+
|
159
|
+
def initialize(jobs_relation)
|
160
|
+
@jobs_relation = jobs_relation
|
161
|
+
end
|
162
|
+
|
163
|
+
def jobs
|
164
|
+
solid_queue_status.finished? ? order_finished_jobs(finished_jobs) : order_executions(executions).map(&:job)
|
165
|
+
end
|
166
|
+
|
167
|
+
def count
|
168
|
+
limit_value_provided? ? direct_count : internally_limited_count
|
169
|
+
end
|
170
|
+
|
171
|
+
def find_job(active_job_id)
|
172
|
+
if job = SolidQueue::Job.find_by(active_job_id: active_job_id)
|
173
|
+
job if matches_relation_filters?(job)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def discard_all
|
178
|
+
execution_class_by_status.discard_all_from_jobs(jobs)
|
179
|
+
end
|
180
|
+
|
181
|
+
def retry_all
|
182
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
attr_reader :jobs_relation
|
187
|
+
|
188
|
+
delegate :queue_name, :limit_value, :limit_value_provided?, :offset_value, :job_class_name, :default_page_size, :worker_id, to: :jobs_relation
|
189
|
+
|
190
|
+
def executions
|
191
|
+
execution_class_by_status.includes(job: "#{solid_queue_status}_execution")
|
192
|
+
.then { |executions| filter_executions_by_queue(executions) }
|
193
|
+
.then { |executions| filter_executions_by_class(executions) }
|
194
|
+
.then { |executions| filter_executions_by_process_id(executions) }
|
195
|
+
.then { |executions| limit(executions) }
|
196
|
+
.then { |executions| offset(executions) }
|
197
|
+
end
|
198
|
+
|
199
|
+
def finished_jobs
|
200
|
+
SolidQueue::Job.finished
|
201
|
+
.then { |jobs| filter_jobs_by_queue(jobs) }
|
202
|
+
.then { |jobs| filter_jobs_by_class(jobs) }
|
203
|
+
.then { |jobs| limit(jobs) }
|
204
|
+
.then { |jobs| offset(jobs) }
|
205
|
+
end
|
206
|
+
|
207
|
+
def order_finished_jobs(jobs)
|
208
|
+
jobs.order(finished_at: :desc)
|
209
|
+
end
|
210
|
+
|
211
|
+
def order_executions(executions)
|
212
|
+
# Follow polling order for scheduled executions, the rest by job_id
|
213
|
+
if solid_queue_status.scheduled? then executions.ordered
|
214
|
+
else
|
215
|
+
executions.order(:job_id)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def matches_relation_filters?(job)
|
220
|
+
matches_status?(job) && matches_queue_name?(job)
|
221
|
+
end
|
222
|
+
|
223
|
+
def direct_count
|
224
|
+
solid_queue_status.finished? ? finished_jobs.count : executions.count
|
225
|
+
end
|
226
|
+
|
227
|
+
INTERNAL_COUNT_LIMIT = 500_000 # Hard limit to keep unlimited count queries fast enough
|
228
|
+
|
229
|
+
def internally_limited_count
|
230
|
+
limited_count = solid_queue_status.finished? ? finished_jobs.limit(INTERNAL_COUNT_LIMIT + 1).count : executions.limit(INTERNAL_COUNT_LIMIT + 1).count
|
231
|
+
(limited_count == INTERNAL_COUNT_LIMIT + 1) ? Float::INFINITY : limited_count
|
232
|
+
end
|
233
|
+
|
234
|
+
def execution_class_by_status
|
235
|
+
if solid_queue_status.present? && !solid_queue_status.finished?
|
236
|
+
"SolidQueue::#{solid_queue_status.capitalize}Execution".safe_constantize
|
237
|
+
else
|
238
|
+
raise ActiveJob::Errors::QueryError, "Status not supported: #{jobs_relation.status}"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def filter_executions_by_queue(executions)
|
243
|
+
return executions unless queue_name.present?
|
244
|
+
|
245
|
+
if solid_queue_status.ready?
|
246
|
+
executions.where(queue_name: queue_name)
|
247
|
+
else
|
248
|
+
executions.where(job: { queue_name: queue_name })
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def filter_jobs_by_queue(jobs)
|
253
|
+
queue_name.present? ? jobs.where(queue_name: queue_name) : jobs
|
254
|
+
end
|
255
|
+
|
256
|
+
def filter_executions_by_class(executions)
|
257
|
+
job_class_name.present? ? executions.where(job: { class_name: job_class_name }) : executions
|
258
|
+
end
|
259
|
+
|
260
|
+
def filter_executions_by_process_id(executions)
|
261
|
+
return executions unless worker_id.present?
|
262
|
+
|
263
|
+
if solid_queue_status.claimed?
|
264
|
+
executions.where(process_id: worker_id)
|
265
|
+
else
|
266
|
+
raise ActiveJob::Errors::QueryError, "Filtering by worker ID is not supported for status #{jobs_relation.status}"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def filter_jobs_by_class(jobs)
|
271
|
+
job_class_name.present? ? jobs.where(class_name: job_class_name) : jobs
|
272
|
+
end
|
273
|
+
|
274
|
+
def limit(executions_or_jobs)
|
275
|
+
limit_value.present? ? executions_or_jobs.limit(limit_value) : executions_or_jobs
|
276
|
+
end
|
277
|
+
|
278
|
+
def offset(executions_or_jobs)
|
279
|
+
offset_value.present? ? executions_or_jobs.offset(offset_value) : executions_or_jobs
|
280
|
+
end
|
281
|
+
|
282
|
+
def matches_status?(job)
|
283
|
+
solid_queue_status.blank? || job.public_send("#{solid_queue_status}?")
|
284
|
+
end
|
285
|
+
|
286
|
+
def matches_queue_name?(job)
|
287
|
+
queue_name.blank? || job.queue_name == queue_name
|
288
|
+
end
|
289
|
+
|
290
|
+
def solid_queue_status
|
291
|
+
STATUS_MAP[jobs_relation.status].to_s.inquiry
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# An enumerable collection of queues that supports direct access to queues by name.
|
2
|
+
#
|
3
|
+
# queue_1 = ApplicationJob::Queue.new("queue_1")
|
4
|
+
# queue_2 = ApplicationJob::Queue.new("queue_2")
|
5
|
+
# queues = ApplicationJob::Queues.new([queue_1, queue_2])
|
6
|
+
#
|
7
|
+
# queues[:queue_1] #=> queue_1
|
8
|
+
# queues[:queue_2] #=> queue_2
|
9
|
+
# queues.to_a #=> [ queue_1, queue_2 ] # Enumerable
|
10
|
+
#
|
11
|
+
# See +ActiveJob::Queue+.
|
12
|
+
class ActiveJob::Queues
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
delegate :each, to: :values
|
16
|
+
delegate :values, to: :queues_by_name, private: true
|
17
|
+
delegate :[], :size, :length, :to_s, :inspect, to: :queues_by_name
|
18
|
+
|
19
|
+
def initialize(queues)
|
20
|
+
@queues_by_name = queues.index_by(&:name).with_indifferent_access
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_h
|
24
|
+
queues_by_name.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
attr_reader :queues_by_name
|
29
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module MissionControl::Jobs::Adapter
|
2
|
+
def activating(&block)
|
3
|
+
block.call
|
4
|
+
end
|
5
|
+
|
6
|
+
def supported_statuses
|
7
|
+
# All adapters need to support these at a minimum
|
8
|
+
[ :pending, :failed ]
|
9
|
+
end
|
10
|
+
|
11
|
+
def supports_filter?(jobs_relation, filter)
|
12
|
+
supported_filters(jobs_relation).include?(filter)
|
13
|
+
end
|
14
|
+
|
15
|
+
# List of filters supported natively. Non-supported filters are done in memory.
|
16
|
+
def supported_filters(jobs_relation)
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
def supports_queue_pausing?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def exposes_workers?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns an array with the list of workers. Each worker is represented as a hash
|
29
|
+
# with these attributes:
|
30
|
+
# {
|
31
|
+
# id: 123,
|
32
|
+
# name: "adapter-name",
|
33
|
+
# hostname: "hey-default-101",
|
34
|
+
# last_heartbeat_at: Fri, 26 Jan 2024 20:31:09.652174000 UTC +00:00,
|
35
|
+
# configuration: { ... }
|
36
|
+
# raw_data: { ... }
|
37
|
+
# }
|
38
|
+
def workers
|
39
|
+
if exposes_workers?
|
40
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `workers`")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns an array with the list of queues. Each queue is represented as a hash
|
45
|
+
# with these attributes:
|
46
|
+
# {
|
47
|
+
# name: "queue_name",
|
48
|
+
# size: 1,
|
49
|
+
# active: true
|
50
|
+
# }
|
51
|
+
def queues
|
52
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `queue_names`")
|
53
|
+
end
|
54
|
+
|
55
|
+
def queue_size(queue_name)
|
56
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `queue_size`")
|
57
|
+
end
|
58
|
+
|
59
|
+
def clear_queue(queue_name)
|
60
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `clear_queue`")
|
61
|
+
end
|
62
|
+
|
63
|
+
def pause_queue(queue_name)
|
64
|
+
if supports_queue_pausing?
|
65
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `pause_queue`")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def resume_queue(queue_name)
|
70
|
+
if supports_queue_pausing?
|
71
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `resume_queue`")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def queue_paused?(queue_name)
|
76
|
+
if supports_queue_pausing?
|
77
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `queue_paused?`")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def jobs_count(jobs_relation)
|
82
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `jobs_count`")
|
83
|
+
end
|
84
|
+
|
85
|
+
def fetch_jobs(jobs_relation)
|
86
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `fetch_jobs`")
|
87
|
+
end
|
88
|
+
|
89
|
+
def retry_all_jobs(jobs_relation)
|
90
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `retry_all_jobs`")
|
91
|
+
end
|
92
|
+
|
93
|
+
def retry_job(job, jobs_relation)
|
94
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `retry_job`")
|
95
|
+
end
|
96
|
+
|
97
|
+
def discard_all_jobs(jobs_relation)
|
98
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `discard_all_jobs`")
|
99
|
+
end
|
100
|
+
|
101
|
+
def discard_job(job, jobs_relation)
|
102
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `discard_job`")
|
103
|
+
end
|
104
|
+
|
105
|
+
def find_job(job_id, *)
|
106
|
+
raise MissionControl::Jobs::Errors::IncompatibleAdapter("Adapter must implement `find_job`")
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# An application containing backend jobs servers
|
2
|
+
class MissionControl::Jobs::Application
|
3
|
+
include MissionControl::Jobs::IdentifiedByName
|
4
|
+
|
5
|
+
attr_reader :servers
|
6
|
+
|
7
|
+
def initialize(name:)
|
8
|
+
super
|
9
|
+
@servers = MissionControl::Jobs::IdentifiedElements.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_servers(queue_adapters_by_name)
|
13
|
+
queue_adapters_by_name.each do |name, queue_adapter|
|
14
|
+
servers << MissionControl::Jobs::Server.new(name: name.to_s, queue_adapter: queue_adapter, application: self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# A container to register applications
|
2
|
+
class MissionControl::Jobs::Applications < MissionControl::Jobs::IdentifiedElements
|
3
|
+
def add(name, queue_adapters_by_name = {})
|
4
|
+
self << MissionControl::Jobs::Application.new(name: name).tap do |application|
|
5
|
+
application.add_servers(queue_adapters_by_name)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MissionControl::Jobs::Console::Helpers
|
2
|
+
def connect_to(server_locator)
|
3
|
+
server = MissionControl::Jobs::Server.from_global_id(server_locator)
|
4
|
+
MissionControl::Jobs::Current.server = server
|
5
|
+
|
6
|
+
puts "Connected to #{server_locator}"
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def jobs_help
|
11
|
+
puts "You are currently connected to #{MissionControl::Jobs::Current.server}" if MissionControl::Jobs::Current.server
|
12
|
+
|
13
|
+
puts "You can connect to a job server with"
|
14
|
+
puts " connect_to <app_id>:<server_id>\n\n"
|
15
|
+
|
16
|
+
puts "Available job servers:\n"
|
17
|
+
|
18
|
+
MissionControl::Jobs.applications.each do |application|
|
19
|
+
application.servers.each do |server|
|
20
|
+
puts " * #{server.to_global_id}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "mission_control/jobs/version"
|
2
|
+
require "mission_control/jobs/engine"
|
3
|
+
|
4
|
+
require "importmap-rails"
|
5
|
+
require "turbo-rails"
|
6
|
+
require "stimulus-rails"
|
7
|
+
|
8
|
+
module MissionControl
|
9
|
+
module Jobs
|
10
|
+
class Engine < ::Rails::Engine
|
11
|
+
isolate_namespace MissionControl::Jobs
|
12
|
+
|
13
|
+
config.mission_control = ActiveSupport::OrderedOptions.new unless config.try(:mission_control)
|
14
|
+
config.mission_control.jobs = ActiveSupport::OrderedOptions.new
|
15
|
+
|
16
|
+
config.before_initialize do
|
17
|
+
config.mission_control.jobs.applications = MissionControl::Jobs::Applications.new
|
18
|
+
|
19
|
+
config.mission_control.jobs.each do |key, value|
|
20
|
+
MissionControl::Jobs.public_send("#{key}=", value)
|
21
|
+
end
|
22
|
+
|
23
|
+
if config.active_job.queue_adapter.present? && MissionControl::Jobs.adapters.empty?
|
24
|
+
MissionControl::Jobs.adapters << config.active_job.queue_adapter
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
initializer "mission_control-jobs.active_job.extensions" do
|
29
|
+
ActiveSupport.on_load :active_job do
|
30
|
+
include ActiveJob::Querying
|
31
|
+
include ActiveJob::Executing
|
32
|
+
include ActiveJob::Failed
|
33
|
+
ActiveJob.extend ActiveJob::Querying::Root
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
config.before_initialize do
|
38
|
+
if MissionControl::Jobs.adapters.include?(:resque)
|
39
|
+
ActiveJob::QueueAdapters::ResqueAdapter.prepend ActiveJob::QueueAdapters::ResqueExt
|
40
|
+
Resque.prepend Resque::ThreadSafeRedis
|
41
|
+
end
|
42
|
+
|
43
|
+
if MissionControl::Jobs.adapters.include?(:solid_queue)
|
44
|
+
ActiveJob::QueueAdapters::SolidQueueAdapter.prepend ActiveJob::QueueAdapters::SolidQueueExt
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
config.after_initialize do |app|
|
49
|
+
unless app.config.eager_load
|
50
|
+
# When loading classes lazily (development), we want to make sure
|
51
|
+
# the base host +ApplicationController+ class is loaded when loading the
|
52
|
+
# Engine's +ApplicationController+, or it will fail to load the class.
|
53
|
+
MissionControl::Jobs.base_controller_class.constantize
|
54
|
+
end
|
55
|
+
|
56
|
+
if MissionControl::Jobs.applications.empty?
|
57
|
+
queue_adapters_by_name = MissionControl::Jobs.adapters.each_with_object({}) do |adapter, hsh|
|
58
|
+
hsh[adapter] = ActiveJob::QueueAdapters.lookup(adapter).new
|
59
|
+
end
|
60
|
+
|
61
|
+
MissionControl::Jobs.applications.add(app.class.module_parent.name, queue_adapters_by_name)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
console do
|
66
|
+
require "irb/context"
|
67
|
+
|
68
|
+
IRB::Context.prepend(MissionControl::Jobs::Console::Context)
|
69
|
+
Rails::ConsoleMethods.include(MissionControl::Jobs::Console::Helpers)
|
70
|
+
|
71
|
+
MissionControl::Jobs.delay_between_bulk_operation_batches = 2
|
72
|
+
MissionControl::Jobs.logger = ActiveSupport::Logger.new(STDOUT)
|
73
|
+
|
74
|
+
puts "\n\nType 'jobs_help' to see how to connect to the available job servers to manage jobs\n\n"
|
75
|
+
end
|
76
|
+
|
77
|
+
initializer "mission_control-jobs.assets" do |app|
|
78
|
+
app.config.assets.paths << root.join("app/javascript")
|
79
|
+
app.config.assets.precompile += %w[ mission_control_jobs_manifest ]
|
80
|
+
end
|
81
|
+
|
82
|
+
initializer "mission_control-jobs.importmap", before: "importmap" do |app|
|
83
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
84
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module MissionControl::Jobs::IdentifiedByName
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
attr_reader :name
|
6
|
+
alias to_s name
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(name:)
|
10
|
+
@name = name.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def id
|
14
|
+
name.parameterize
|
15
|
+
end
|
16
|
+
|
17
|
+
alias to_param id
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# A collection of elements offering a Hash-like access based on
|
2
|
+
# their +id+.
|
3
|
+
class MissionControl::Jobs::IdentifiedElements
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
delegate :[], :empty?, to: :elements
|
7
|
+
delegate :each, :last, :length, to: :to_a
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@elements = HashWithIndifferentAccess.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(item)
|
14
|
+
@elements[item.id] = item
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_a
|
18
|
+
@elements.values
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
attr_reader :elements
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module MissionControl::Jobs::Server::Serializable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class_methods do
|
5
|
+
# Loads a server from a locator string with the format +<application>:<server>+. For example:
|
6
|
+
#
|
7
|
+
# bc4:resque_chicago
|
8
|
+
#
|
9
|
+
# When the +<server>+ fragment is omitted it will return the first server for the application.
|
10
|
+
def from_global_id(global_id)
|
11
|
+
app_id, server_id = global_id.split(":")
|
12
|
+
|
13
|
+
application = MissionControl::Jobs.applications[app_id] or raise MissionControl::Jobs::Errors::ResourceNotFound, "No application with id #{app_id} found"
|
14
|
+
server = server_id ? application.servers[server_id] : application.servers.first
|
15
|
+
|
16
|
+
server or raise MissionControl::Jobs::Errors::ResourceNotFound, "No server for #{global_id} found"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_global_id
|
21
|
+
suffix = ":#{id}" if application.servers.length > 1
|
22
|
+
"#{application&.id}#{suffix}"
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MissionControl::Jobs::Server::Workers
|
2
|
+
def workers
|
3
|
+
queue_adapter.workers.collect do |worker|
|
4
|
+
MissionControl::Jobs::Worker.new(queue_adapter: queue_adapter, **worker)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def find_worker(worker_id)
|
9
|
+
if worker = queue_adapter.find_worker(worker_id)
|
10
|
+
MissionControl::Jobs::Worker.new(queue_adapter: queue_adapter, **worker)
|
11
|
+
else
|
12
|
+
raise MissionControl::Jobs::Errors::ResourceNotFound, "No worker found with ID #{worker_id}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|