mission_control-jobs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +244 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/mission_control_jobs_manifest.js +4 -0
  6. data/app/assets/stylesheets/mission_control/jobs/application.css +16 -0
  7. data/app/assets/stylesheets/mission_control/jobs/forms.css +8 -0
  8. data/app/assets/stylesheets/mission_control/jobs/jobs.css +7 -0
  9. data/app/controllers/concerns/mission_control/jobs/adapter_features.rb +20 -0
  10. data/app/controllers/concerns/mission_control/jobs/application_scoped.rb +38 -0
  11. data/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb +17 -0
  12. data/app/controllers/concerns/mission_control/jobs/job_filters.rb +18 -0
  13. data/app/controllers/concerns/mission_control/jobs/job_scoped.rb +16 -0
  14. data/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb +25 -0
  15. data/app/controllers/concerns/mission_control/jobs/queue_scoped.rb +12 -0
  16. data/app/controllers/mission_control/jobs/application_controller.rb +11 -0
  17. data/app/controllers/mission_control/jobs/bulk_discards_controller.rb +20 -0
  18. data/app/controllers/mission_control/jobs/bulk_retries_controller.rb +10 -0
  19. data/app/controllers/mission_control/jobs/discards_controller.rb +13 -0
  20. data/app/controllers/mission_control/jobs/jobs_controller.rb +37 -0
  21. data/app/controllers/mission_control/jobs/queues/pauses_controller.rb +15 -0
  22. data/app/controllers/mission_control/jobs/queues_controller.rb +24 -0
  23. data/app/controllers/mission_control/jobs/retries_controller.rb +13 -0
  24. data/app/controllers/mission_control/jobs/workers_controller.rb +18 -0
  25. data/app/helpers/mission_control/jobs/application_helper.rb +8 -0
  26. data/app/helpers/mission_control/jobs/dates_helper.rb +19 -0
  27. data/app/helpers/mission_control/jobs/jobs_helper.rb +63 -0
  28. data/app/helpers/mission_control/jobs/navigation_helper.rb +51 -0
  29. data/app/helpers/mission_control/jobs/ui_helper.rb +23 -0
  30. data/app/javascript/mission_control/jobs/application.js +4 -0
  31. data/app/javascript/mission_control/jobs/controllers/application.js +9 -0
  32. data/app/javascript/mission_control/jobs/controllers/form_controller.js +21 -0
  33. data/app/javascript/mission_control/jobs/controllers/index.js +11 -0
  34. data/app/javascript/mission_control/jobs/helpers/debounce_helpers.js +9 -0
  35. data/app/javascript/mission_control/jobs/helpers/index.js +1 -0
  36. data/app/jobs/mission_control/jobs/application_job.rb +6 -0
  37. data/app/mailers/mission_control/jobs/application_mailer.rb +8 -0
  38. data/app/models/mission_control/jobs/application_record.rb +7 -0
  39. data/app/models/mission_control/jobs/current.rb +3 -0
  40. data/app/models/mission_control/jobs/page.rb +48 -0
  41. data/app/models/mission_control/jobs/worker.rb +17 -0
  42. data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +11 -0
  43. data/app/views/layouts/mission_control/jobs/_flash.html.erb +9 -0
  44. data/app/views/layouts/mission_control/jobs/_navigation.html.erb +9 -0
  45. data/app/views/layouts/mission_control/jobs/application.html.erb +25 -0
  46. data/app/views/layouts/mission_control/jobs/application_selection/_applications.html.erb +13 -0
  47. data/app/views/layouts/mission_control/jobs/application_selection/_servers.html.erb +15 -0
  48. data/app/views/mission_control/jobs/jobs/_error_information.html.erb +19 -0
  49. data/app/views/mission_control/jobs/jobs/_filters.html.erb +35 -0
  50. data/app/views/mission_control/jobs/jobs/_general_information.html.erb +54 -0
  51. data/app/views/mission_control/jobs/jobs/_job.html.erb +13 -0
  52. data/app/views/mission_control/jobs/jobs/_jobs_page.html.erb +15 -0
  53. data/app/views/mission_control/jobs/jobs/_raw_data.html.erb +4 -0
  54. data/app/views/mission_control/jobs/jobs/_title.html.erb +13 -0
  55. data/app/views/mission_control/jobs/jobs/_toolbar.html.erb +18 -0
  56. data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +3 -0
  57. data/app/views/mission_control/jobs/jobs/failed/_actions.html.erb +5 -0
  58. data/app/views/mission_control/jobs/jobs/failed/_job.html.erb +7 -0
  59. data/app/views/mission_control/jobs/jobs/finished/_job.html.erb +2 -0
  60. data/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +9 -0
  61. data/app/views/mission_control/jobs/jobs/index.html.erb +19 -0
  62. data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +7 -0
  63. data/app/views/mission_control/jobs/jobs/show.html.erb +6 -0
  64. data/app/views/mission_control/jobs/queues/_actions.html.erb +7 -0
  65. data/app/views/mission_control/jobs/queues/_job.html.erb +15 -0
  66. data/app/views/mission_control/jobs/queues/_queue.html.erb +16 -0
  67. data/app/views/mission_control/jobs/queues/_queue_title.html.erb +17 -0
  68. data/app/views/mission_control/jobs/queues/index.html.erb +16 -0
  69. data/app/views/mission_control/jobs/queues/show.html.erb +25 -0
  70. data/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb +5 -0
  71. data/app/views/mission_control/jobs/workers/_configuration.html.erb +6 -0
  72. data/app/views/mission_control/jobs/workers/_job.html.erb +19 -0
  73. data/app/views/mission_control/jobs/workers/_jobs.html.erb +20 -0
  74. data/app/views/mission_control/jobs/workers/_raw_data.html.erb +6 -0
  75. data/app/views/mission_control/jobs/workers/_title.html.erb +11 -0
  76. data/app/views/mission_control/jobs/workers/_worker.html.erb +21 -0
  77. data/app/views/mission_control/jobs/workers/index.html.erb +17 -0
  78. data/app/views/mission_control/jobs/workers/show.html.erb +7 -0
  79. data/config/importmap.rb +6 -0
  80. data/config/routes.rb +33 -0
  81. data/lib/active_job/errors/invalid_operation.rb +5 -0
  82. data/lib/active_job/errors/job_not_found_error.rb +14 -0
  83. data/lib/active_job/errors/query_error.rb +5 -0
  84. data/lib/active_job/executing.rb +43 -0
  85. data/lib/active_job/execution_error.rb +8 -0
  86. data/lib/active_job/failed.rb +11 -0
  87. data/lib/active_job/job_proxy.rb +26 -0
  88. data/lib/active_job/jobs_relation.rb +300 -0
  89. data/lib/active_job/querying.rb +44 -0
  90. data/lib/active_job/queue.rb +62 -0
  91. data/lib/active_job/queue_adapters/resque_ext.rb +300 -0
  92. data/lib/active_job/queue_adapters/solid_queue_ext.rb +294 -0
  93. data/lib/active_job/queues.rb +29 -0
  94. data/lib/mission_control/jobs/adapter.rb +108 -0
  95. data/lib/mission_control/jobs/application.rb +17 -0
  96. data/lib/mission_control/jobs/applications.rb +8 -0
  97. data/lib/mission_control/jobs/console/context.rb +11 -0
  98. data/lib/mission_control/jobs/console/helpers.rb +26 -0
  99. data/lib/mission_control/jobs/engine.rb +88 -0
  100. data/lib/mission_control/jobs/errors/incompatible_adapter.rb +2 -0
  101. data/lib/mission_control/jobs/errors/resource_not_found.rb +2 -0
  102. data/lib/mission_control/jobs/identified_by_name.rb +18 -0
  103. data/lib/mission_control/jobs/identified_elements.rb +23 -0
  104. data/lib/mission_control/jobs/server/serializable.rb +24 -0
  105. data/lib/mission_control/jobs/server/workers.rb +15 -0
  106. data/lib/mission_control/jobs/server.rb +26 -0
  107. data/lib/mission_control/jobs/version.rb +5 -0
  108. data/lib/mission_control/jobs.rb +19 -0
  109. data/lib/resque/thread_safe_redis.rb +34 -0
  110. data/lib/tasks/mission_control/jobs_tasks.rake +4 -0
  111. 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,11 @@
1
+ module MissionControl::Jobs::Console::Context
2
+ mattr_accessor :jobs_server
3
+
4
+ def evaluate(*)
5
+ if MissionControl::Jobs::Current.server
6
+ MissionControl::Jobs::Current.server.activating { super }
7
+ else
8
+ super
9
+ end
10
+ end
11
+ 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,2 @@
1
+ class MissionControl::Jobs::Errors::IncompatibleAdapter < StandardError
2
+ end
@@ -0,0 +1,2 @@
1
+ class MissionControl::Jobs::Errors::ResourceNotFound < StandardError
2
+ 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