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.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +106 -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/bulma.min.css +21013 -0
  8. data/app/assets/stylesheets/mission_control/jobs/forms.css +8 -0
  9. data/app/assets/stylesheets/mission_control/jobs/jobs.css +68 -0
  10. data/app/controllers/concerns/mission_control/jobs/adapter_features.rb +24 -0
  11. data/app/controllers/concerns/mission_control/jobs/application_scoped.rb +38 -0
  12. data/app/controllers/concerns/mission_control/jobs/basic_authentication.rb +33 -0
  13. data/app/controllers/concerns/mission_control/jobs/failed_jobs_bulk_operations.rb +17 -0
  14. data/app/controllers/concerns/mission_control/jobs/job_filters.rb +41 -0
  15. data/app/controllers/concerns/mission_control/jobs/job_scoped.rb +16 -0
  16. data/app/controllers/concerns/mission_control/jobs/not_found_redirections.rb +36 -0
  17. data/app/controllers/concerns/mission_control/jobs/queue_scoped.rb +12 -0
  18. data/app/controllers/mission_control/jobs/application_controller.rb +32 -0
  19. data/app/controllers/mission_control/jobs/bulk_discards_controller.rb +20 -0
  20. data/app/controllers/mission_control/jobs/bulk_retries_controller.rb +10 -0
  21. data/app/controllers/mission_control/jobs/discards_controller.rb +18 -0
  22. data/app/controllers/mission_control/jobs/dispatches_controller.rb +18 -0
  23. data/app/controllers/mission_control/jobs/jobs_controller.rb +54 -0
  24. data/app/controllers/mission_control/jobs/progresses_controller.rb +27 -0
  25. data/app/controllers/mission_control/jobs/queues/pauses_controller.rb +15 -0
  26. data/app/controllers/mission_control/jobs/queues_controller.rb +16 -0
  27. data/app/controllers/mission_control/jobs/recurring_tasks_controller.rb +38 -0
  28. data/app/controllers/mission_control/jobs/retries_controller.rb +13 -0
  29. data/app/controllers/mission_control/jobs/stops_controller.rb +22 -0
  30. data/app/controllers/mission_control/jobs/workers_controller.rb +23 -0
  31. data/app/helpers/mission_control/jobs/application_helper.rb +9 -0
  32. data/app/helpers/mission_control/jobs/dates_helper.rb +19 -0
  33. data/app/helpers/mission_control/jobs/interface_helper.rb +23 -0
  34. data/app/helpers/mission_control/jobs/jobs_helper.rb +94 -0
  35. data/app/helpers/mission_control/jobs/navigation_helper.rb +60 -0
  36. data/app/javascript/mission_control/jobs/application.js +4 -0
  37. data/app/javascript/mission_control/jobs/controllers/application.js +9 -0
  38. data/app/javascript/mission_control/jobs/controllers/form_controller.js +21 -0
  39. data/app/javascript/mission_control/jobs/controllers/index.js +11 -0
  40. data/app/javascript/mission_control/jobs/controllers/progress_controller.js +55 -0
  41. data/app/javascript/mission_control/jobs/helpers/debounce_helpers.js +9 -0
  42. data/app/javascript/mission_control/jobs/helpers/index.js +1 -0
  43. data/app/models/mission_control/jobs/application_record.rb +7 -0
  44. data/app/models/mission_control/jobs/current.rb +3 -0
  45. data/app/models/mission_control/jobs/job_progress.rb +7 -0
  46. data/app/models/mission_control/jobs/page.rb +48 -0
  47. data/app/models/mission_control/jobs/recurring_task.rb +25 -0
  48. data/app/models/mission_control/jobs/worker.rb +17 -0
  49. data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +14 -0
  50. data/app/views/layouts/mission_control/jobs/_flash.html.erb +9 -0
  51. data/app/views/layouts/mission_control/jobs/_navigation.html.erb +9 -0
  52. data/app/views/layouts/mission_control/jobs/application.html.erb +27 -0
  53. data/app/views/layouts/mission_control/jobs/application_selection/_applications.html.erb +13 -0
  54. data/app/views/layouts/mission_control/jobs/application_selection/_servers.html.erb +15 -0
  55. data/app/views/mission_control/jobs/jobs/_error_information.html.erb +22 -0
  56. data/app/views/mission_control/jobs/jobs/_filters.html.erb +55 -0
  57. data/app/views/mission_control/jobs/jobs/_general_information.html.erb +85 -0
  58. data/app/views/mission_control/jobs/jobs/_job.html.erb +13 -0
  59. data/app/views/mission_control/jobs/jobs/_jobs_page.html.erb +13 -0
  60. data/app/views/mission_control/jobs/jobs/_progress_bar.html.erb +16 -0
  61. data/app/views/mission_control/jobs/jobs/_raw_data.html.erb +4 -0
  62. data/app/views/mission_control/jobs/jobs/_title.html.erb +22 -0
  63. data/app/views/mission_control/jobs/jobs/_toolbar.html.erb +18 -0
  64. data/app/views/mission_control/jobs/jobs/blocked/_actions.html.erb +3 -0
  65. data/app/views/mission_control/jobs/jobs/blocked/_job.html.erb +8 -0
  66. data/app/views/mission_control/jobs/jobs/failed/_actions.html.erb +5 -0
  67. data/app/views/mission_control/jobs/jobs/failed/_backtrace_toggle.html.erb +14 -0
  68. data/app/views/mission_control/jobs/jobs/failed/_job.html.erb +7 -0
  69. data/app/views/mission_control/jobs/jobs/finished/_job.html.erb +2 -0
  70. data/app/views/mission_control/jobs/jobs/in_progress/_actions.html.erb +6 -0
  71. data/app/views/mission_control/jobs/jobs/in_progress/_job.html.erb +13 -0
  72. data/app/views/mission_control/jobs/jobs/index.html.erb +19 -0
  73. data/app/views/mission_control/jobs/jobs/scheduled/_actions.html.erb +5 -0
  74. data/app/views/mission_control/jobs/jobs/scheduled/_job.html.erb +13 -0
  75. data/app/views/mission_control/jobs/jobs/show.html.erb +6 -0
  76. data/app/views/mission_control/jobs/queues/_actions.html.erb +7 -0
  77. data/app/views/mission_control/jobs/queues/_job.html.erb +15 -0
  78. data/app/views/mission_control/jobs/queues/_queue.html.erb +16 -0
  79. data/app/views/mission_control/jobs/queues/_queue_title.html.erb +17 -0
  80. data/app/views/mission_control/jobs/queues/index.html.erb +16 -0
  81. data/app/views/mission_control/jobs/queues/show.html.erb +25 -0
  82. data/app/views/mission_control/jobs/recurring_tasks/_actions.html.erb +5 -0
  83. data/app/views/mission_control/jobs/recurring_tasks/_general_information.html.erb +37 -0
  84. data/app/views/mission_control/jobs/recurring_tasks/_recurring_task.html.erb +22 -0
  85. data/app/views/mission_control/jobs/recurring_tasks/_title.html.erb +10 -0
  86. data/app/views/mission_control/jobs/recurring_tasks/index.html.erb +18 -0
  87. data/app/views/mission_control/jobs/recurring_tasks/show.html.erb +14 -0
  88. data/app/views/mission_control/jobs/shared/_job.html.erb +27 -0
  89. data/app/views/mission_control/jobs/shared/_jobs.html.erb +14 -0
  90. data/app/views/mission_control/jobs/shared/_pagination_toolbar.html.erb +7 -0
  91. data/app/views/mission_control/jobs/workers/_configuration.html.erb +6 -0
  92. data/app/views/mission_control/jobs/workers/_raw_data.html.erb +6 -0
  93. data/app/views/mission_control/jobs/workers/_title.html.erb +11 -0
  94. data/app/views/mission_control/jobs/workers/_worker.html.erb +21 -0
  95. data/app/views/mission_control/jobs/workers/_workers_page.html.erb +15 -0
  96. data/app/views/mission_control/jobs/workers/index.html.erb +6 -0
  97. data/app/views/mission_control/jobs/workers/show.html.erb +14 -0
  98. data/config/importmap.rb +6 -0
  99. data/config/routes.rb +35 -0
  100. data/db/migrate/20260828105144_create_mission_control_job_progresses.rb +16 -0
  101. data/lib/active_job/errors/invalid_operation.rb +5 -0
  102. data/lib/active_job/errors/job_not_found_error.rb +14 -0
  103. data/lib/active_job/errors/query_error.rb +5 -0
  104. data/lib/active_job/executing.rb +40 -0
  105. data/lib/active_job/execution_error.rb +8 -0
  106. data/lib/active_job/failed.rb +7 -0
  107. data/lib/active_job/job_proxy.rb +38 -0
  108. data/lib/active_job/jobs_relation.rb +318 -0
  109. data/lib/active_job/progress_reporting.rb +21 -0
  110. data/lib/active_job/querying.rb +44 -0
  111. data/lib/active_job/queue.rb +62 -0
  112. data/lib/active_job/queue_adapters/async_ext.rb +49 -0
  113. data/lib/active_job/queue_adapters/resque_ext.rb +317 -0
  114. data/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb +52 -0
  115. data/lib/active_job/queue_adapters/solid_queue_ext/workers.rb +41 -0
  116. data/lib/active_job/queue_adapters/solid_queue_ext.rb +313 -0
  117. data/lib/active_job/queues.rb +33 -0
  118. data/lib/mission_control/jobs/adapter.rb +156 -0
  119. data/lib/mission_control/jobs/application.rb +20 -0
  120. data/lib/mission_control/jobs/applications.rb +8 -0
  121. data/lib/mission_control/jobs/arguments_filter.rb +24 -0
  122. data/lib/mission_control/jobs/authentication.rb +67 -0
  123. data/lib/mission_control/jobs/console/connect_to.rb +15 -0
  124. data/lib/mission_control/jobs/console/context.rb +11 -0
  125. data/lib/mission_control/jobs/console/jobs_help.rb +23 -0
  126. data/lib/mission_control/jobs/engine.rb +118 -0
  127. data/lib/mission_control/jobs/errors/incompatible_adapter.rb +2 -0
  128. data/lib/mission_control/jobs/errors/resource_not_found.rb +2 -0
  129. data/lib/mission_control/jobs/i18n_config.rb +17 -0
  130. data/lib/mission_control/jobs/identified_by_name.rb +18 -0
  131. data/lib/mission_control/jobs/identified_elements.rb +23 -0
  132. data/lib/mission_control/jobs/job_stopped_error.rb +5 -0
  133. data/lib/mission_control/jobs/progress/null_store.rb +25 -0
  134. data/lib/mission_control/jobs/progress/reporter.rb +63 -0
  135. data/lib/mission_control/jobs/progress/store.rb +72 -0
  136. data/lib/mission_control/jobs/progress.rb +17 -0
  137. data/lib/mission_control/jobs/server/recurring_tasks.rb +15 -0
  138. data/lib/mission_control/jobs/server/serializable.rb +24 -0
  139. data/lib/mission_control/jobs/server/workers.rb +13 -0
  140. data/lib/mission_control/jobs/server.rb +27 -0
  141. data/lib/mission_control/jobs/tasks.rb +8 -0
  142. data/lib/mission_control/jobs/version.rb +5 -0
  143. data/lib/mission_control/jobs/workers_relation.rb +78 -0
  144. data/lib/mission_control/jobs.rb +43 -0
  145. data/lib/resque/thread_safe_redis.rb +34 -0
  146. data/lib/solid_job_monitor.rb +6 -0
  147. metadata +527 -0
@@ -0,0 +1,313 @@
1
+ module ActiveJob::QueueAdapters::SolidQueueExt
2
+ include MissionControl::Jobs::Adapter
3
+ include RecurringTasks, Workers
4
+
5
+ def queues
6
+ queues = SolidQueue::Queue.all
7
+ queue_names = queues.map(&:name)
8
+ pauses = SolidQueue::Pause.where(queue_name: queue_names).index_by(&:queue_name)
9
+ sizes = SolidQueue::ReadyExecution.where(queue_name: queue_names).group(:queue_name).count
10
+
11
+ queues.collect do |queue|
12
+ {
13
+ name: queue.name,
14
+ size: sizes[queue.name] || 0,
15
+ active: pauses[queue.name].nil?
16
+ }
17
+ end
18
+ end
19
+
20
+ def queue_size(queue_name)
21
+ find_queue_by_name(queue_name).size
22
+ end
23
+
24
+ def clear_queue(queue_name)
25
+ find_queue_by_name(queue_name).clear
26
+ end
27
+
28
+ def pause_queue(queue_name)
29
+ find_queue_by_name(queue_name).pause
30
+ end
31
+
32
+ def resume_queue(queue_name)
33
+ find_queue_by_name(queue_name).resume
34
+ end
35
+
36
+ def queue_paused?(queue_name)
37
+ find_queue_by_name(queue_name).paused?
38
+ end
39
+
40
+ def supported_job_statuses
41
+ SolidQueueJobs::STATUS_MAP.keys
42
+ end
43
+
44
+ def supported_job_filters(*)
45
+ [ :queue_name, :job_class_name, :finished_at ]
46
+ end
47
+
48
+ def jobs_count(jobs_relation)
49
+ SolidQueueJobs.new(jobs_relation).count
50
+ end
51
+
52
+ def fetch_jobs(jobs_relation)
53
+ SolidQueueJobs.new(jobs_relation).jobs.map do |job|
54
+ deserialize_and_proxy_solid_queue_job(job, jobs_relation.status)
55
+ end
56
+ end
57
+
58
+ def retry_all_jobs(jobs_relation)
59
+ SolidQueueJobs.new(jobs_relation).retry_all
60
+ end
61
+
62
+ def retry_job(job, jobs_relation)
63
+ find_solid_queue_job!(job.job_id, jobs_relation).retry
64
+ end
65
+
66
+ def discard_all_jobs(jobs_relation)
67
+ SolidQueueJobs.new(jobs_relation).discard_all
68
+ end
69
+
70
+ def discard_job(job, jobs_relation)
71
+ find_solid_queue_job!(job.job_id, jobs_relation).discard
72
+ end
73
+
74
+ def dispatch_job(job, jobs_relation)
75
+ dispatch_immediately find_solid_queue_job!(job.job_id, jobs_relation)
76
+ end
77
+
78
+ def find_job(job_id, *)
79
+ if job = SolidQueue::Job.where(active_job_id: job_id).order(:id).last
80
+ deserialize_and_proxy_solid_queue_job job
81
+ end
82
+ end
83
+
84
+ private
85
+ def find_queue_by_name(queue_name)
86
+ SolidQueue::Queue.find_by_name(queue_name)
87
+ end
88
+
89
+ def find_solid_queue_job!(job_id, jobs_relation)
90
+ find_solid_queue_job(job_id, jobs_relation) or raise ActiveJob::Errors::JobNotFoundError.new(job_id, jobs_relation)
91
+ end
92
+
93
+ def find_solid_queue_job(job_id, jobs_relation)
94
+ SolidQueueJobs.new(jobs_relation).find_job(job_id)
95
+ end
96
+
97
+ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
98
+ job_status ||= status_from_solid_queue_job(solid_queue_job)
99
+
100
+ ActiveJob::JobProxy.new(solid_queue_job.arguments).tap do |job|
101
+ job.status = job_status
102
+ job.last_execution_error = execution_error_from_solid_queue_job(solid_queue_job) if job_status == :failed
103
+ job.raw_data = solid_queue_job.as_json
104
+ job.filtered_raw_data = filter_raw_data_arguments(job.raw_data)
105
+ job.failed_at = solid_queue_job&.failed_execution&.created_at if job_status == :failed
106
+ job.finished_at = solid_queue_job.finished_at
107
+ job.blocked_by = solid_queue_job.concurrency_key
108
+ job.blocked_until = solid_queue_job&.blocked_execution&.expires_at if job_status == :blocked
109
+ job.worker_id = solid_queue_job&.claimed_execution&.process_id if job_status == :in_progress
110
+ job.started_at = solid_queue_job&.claimed_execution&.created_at if job_status == :in_progress
111
+ job.scheduled_at = solid_queue_job.scheduled_at
112
+ end
113
+ end
114
+
115
+ def filter_raw_data_arguments(raw_data)
116
+ raw_data.deep_dup.tap do |filtered_raw_data|
117
+ filtered_raw_data["arguments"]["arguments"] = MissionControl::Jobs.job_arguments_filter.apply_to(filtered_raw_data.dig("arguments", "arguments"))
118
+ end
119
+ end
120
+
121
+ def status_from_solid_queue_job(solid_queue_job)
122
+ SolidQueueJobs::STATUS_MAP.invert[solid_queue_job.status]
123
+ end
124
+
125
+ def execution_error_from_solid_queue_job(solid_queue_job)
126
+ if solid_queue_job.failed?
127
+ ActiveJob::ExecutionError.new \
128
+ error_class: solid_queue_job.failed_execution.exception_class,
129
+ message: solid_queue_job.failed_execution.message,
130
+ backtrace: solid_queue_job.failed_execution.backtrace || []
131
+ end
132
+ rescue JSON::ParserError
133
+ ActiveJob::ExecutionError.new \
134
+ error_class: "",
135
+ message: solid_queue_job.failed_execution.error_before_type_cast,
136
+ backtrace: []
137
+ end
138
+
139
+ def dispatch_immediately(job)
140
+ if job.blocked?
141
+ SolidQueue::Job.transaction do
142
+ job.dispatch_bypassing_concurrency_limits
143
+ job.blocked_execution.destroy!
144
+ end
145
+ else
146
+ job.scheduled_execution.update!(scheduled_at: Time.now)
147
+ end
148
+ end
149
+
150
+ class SolidQueueJobs
151
+ STATUS_MAP = {
152
+ pending: :ready,
153
+ failed: :failed,
154
+ in_progress: :claimed,
155
+ blocked: :blocked,
156
+ scheduled: :scheduled,
157
+ finished: :finished
158
+ }
159
+
160
+ def initialize(jobs_relation)
161
+ @jobs_relation = jobs_relation
162
+ end
163
+
164
+ def jobs
165
+ solid_queue_status.finished? ? order_finished_jobs(finished_jobs) : order_executions(executions).map(&:job).compact
166
+ end
167
+
168
+ def count
169
+ limit_value_provided? ? direct_count : internally_limited_count
170
+ end
171
+
172
+ def find_job(active_job_id)
173
+ if job = SolidQueue::Job.where(active_job_id: active_job_id).order(:id).last
174
+ job if matches_relation_filters?(job)
175
+ end
176
+ end
177
+
178
+ def discard_all
179
+ execution_class_by_status.discard_all_from_jobs(jobs)
180
+ end
181
+
182
+ def retry_all
183
+ SolidQueue::FailedExecution.retry_all(jobs)
184
+ end
185
+
186
+ private
187
+ attr_reader :jobs_relation
188
+
189
+ delegate :queue_name, :limit_value, :limit_value_provided?, :offset_value, :job_class_name,
190
+ :default_page_size, :worker_id, :recurring_task_id, :finished_at, to: :jobs_relation
191
+
192
+ def executions
193
+ execution_class_by_status
194
+ .then { |executions| include_execution_association(executions) }
195
+ .then { |executions| filter_executions_by_queue(executions) }
196
+ .then { |executions| filter_executions_by_class(executions) }
197
+ .then { |executions| filter_executions_by_process_id(executions) }
198
+ .then { |executions| filter_executions_by_task_key(executions) }
199
+ .then { |executions| limit(executions) }
200
+ .then { |executions| offset(executions) }
201
+ end
202
+
203
+ def finished_jobs
204
+ SolidQueue::Job.finished
205
+ .then { |jobs| filter_jobs_by_queue(jobs) }
206
+ .then { |jobs| filter_jobs_by_class(jobs) }
207
+ .then { |jobs| filter_jobs_by_finished_at(jobs) }
208
+ .then { |jobs| limit(jobs) }
209
+ .then { |jobs| offset(jobs) }
210
+ end
211
+
212
+ def order_finished_jobs(jobs)
213
+ jobs.order(finished_at: :desc)
214
+ end
215
+
216
+ def order_executions(executions)
217
+ case
218
+ # Follow polling order for scheduled executions, the rest by job_id, desc or asc
219
+ when solid_queue_status.scheduled? then executions.ordered
220
+ when recurring_task_id.present? then executions.order(job_id: :desc)
221
+ else executions.order(job_id: :asc)
222
+ end
223
+ end
224
+
225
+ def matches_relation_filters?(job)
226
+ matches_status?(job) && matches_queue_name?(job)
227
+ end
228
+
229
+ def direct_count
230
+ solid_queue_status.finished? ? finished_jobs.count : executions.count
231
+ end
232
+
233
+ def internally_limited_count
234
+ count_limit = MissionControl::Jobs.internal_query_count_limit + 1
235
+ limited_count = solid_queue_status.finished? ? finished_jobs.limit(count_limit).count : executions.limit(count_limit).count
236
+ (limited_count == count_limit) ? Float::INFINITY : limited_count
237
+ end
238
+
239
+ def execution_class_by_status
240
+ if recurring_task_id.present?
241
+ SolidQueue::RecurringExecution
242
+ elsif solid_queue_status.present? && !solid_queue_status.finished?
243
+ "SolidQueue::#{solid_queue_status.capitalize}Execution".safe_constantize
244
+ else
245
+ raise ActiveJob::Errors::QueryError, "Status not supported: #{jobs_relation.status}"
246
+ end
247
+ end
248
+
249
+ def include_execution_association(executions)
250
+ solid_queue_status.present? ? executions.includes(job: "#{solid_queue_status}_execution") : executions.includes(:job)
251
+ end
252
+
253
+ def filter_executions_by_queue(executions)
254
+ return executions unless queue_name.present?
255
+
256
+ if solid_queue_status.ready?
257
+ executions.where(queue_name: queue_name)
258
+ else
259
+ executions.where(job: { queue_name: queue_name })
260
+ end
261
+ end
262
+
263
+ def filter_jobs_by_queue(jobs)
264
+ queue_name.present? ? jobs.where(queue_name: queue_name) : jobs
265
+ end
266
+
267
+ def filter_executions_by_class(executions)
268
+ job_class_name.present? ? executions.where(job: { class_name: job_class_name }) : executions
269
+ end
270
+
271
+ def filter_executions_by_process_id(executions)
272
+ return executions unless worker_id.present?
273
+
274
+ if solid_queue_status.claimed?
275
+ executions.where(process_id: worker_id)
276
+ else
277
+ raise ActiveJob::Errors::QueryError, "Filtering by worker id is not supported for status #{jobs_relation.status}"
278
+ end
279
+ end
280
+
281
+ def filter_executions_by_task_key(executions)
282
+ recurring_task_id.present? ? executions.where(task_key: recurring_task_id) : executions
283
+ end
284
+
285
+ def filter_jobs_by_class(jobs)
286
+ job_class_name.present? ? jobs.where(class_name: job_class_name) : jobs
287
+ end
288
+
289
+ def filter_jobs_by_finished_at(jobs)
290
+ finished_at.present? ? jobs.where(finished_at: finished_at) : jobs
291
+ end
292
+
293
+ def limit(executions_or_jobs)
294
+ limit_value.present? ? executions_or_jobs.limit(limit_value) : executions_or_jobs
295
+ end
296
+
297
+ def offset(executions_or_jobs)
298
+ offset_value.present? ? executions_or_jobs.offset(offset_value) : executions_or_jobs
299
+ end
300
+
301
+ def matches_status?(job)
302
+ solid_queue_status.blank? || job.public_send("#{solid_queue_status}?")
303
+ end
304
+
305
+ def matches_queue_name?(job)
306
+ queue_name.blank? || job.queue_name == queue_name
307
+ end
308
+
309
+ def solid_queue_status
310
+ STATUS_MAP[jobs_relation.status].to_s.inquiry
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,33 @@
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_id, private: true
17
+ delegate :size, :length, :to_s, :inspect, to: :queues_by_id
18
+
19
+ def initialize(queues)
20
+ @queues_by_id = queues.index_by(&:id).with_indifferent_access
21
+ end
22
+
23
+ def to_h
24
+ queues_by_id.dup
25
+ end
26
+
27
+ def [](name)
28
+ queues_by_id[name.to_s.parameterize]
29
+ end
30
+
31
+ private
32
+ attr_reader :queues_by_id
33
+ end
@@ -0,0 +1,156 @@
1
+ module MissionControl::Jobs::Adapter
2
+ def activating(&block)
3
+ block.call
4
+ end
5
+
6
+ def supports_job_status?(status)
7
+ supported_job_statuses.include?(status)
8
+ end
9
+
10
+ def supported_job_statuses
11
+ # All adapters need to support these at a minimum
12
+ [ :pending, :failed ]
13
+ end
14
+
15
+ def supports_job_filter?(jobs_relation, filter)
16
+ supported_job_filters(jobs_relation).include?(filter)
17
+ end
18
+
19
+ # List of filters supported natively. Non-supported filters are done in memory.
20
+ def supported_job_filters(jobs_relation)
21
+ []
22
+ end
23
+
24
+ def supports_queue_pausing?
25
+ true
26
+ end
27
+
28
+ def exposes_workers?
29
+ false
30
+ end
31
+
32
+ def supports_recurring_tasks?
33
+ false
34
+ end
35
+
36
+ # Returns an array with the list of recurring tasks. Each task is represented as a hash
37
+ # with these attributes:
38
+ # {
39
+ # id: "periodic-job",
40
+ # job_class_name: "MyJob",
41
+ # arguments: [ 123, { arg: :value }]
42
+ # schedule: "every monday at 9 am",
43
+ # last_enqueued_at: Fri, 26 Jan 2024 20:31:09.652174000 UTC +00:00,
44
+ # }
45
+ def recurring_tasks
46
+ if supports_recurring_tasks?
47
+ raise_incompatible_adapter_error_from :recurring_tasks
48
+ end
49
+ end
50
+
51
+ # Returns a recurring task represented by a hash as indicated above
52
+ def find_recurring_task(recurring_task_id)
53
+ if supports_recurring_tasks?
54
+ raise_incompatible_adapter_error_from :find_recurring_task
55
+ end
56
+ end
57
+
58
+
59
+ # Returns an array with the list of workers. Each worker is represented as a hash
60
+ # with these attributes:
61
+ # {
62
+ # id: 123,
63
+ # name: "worker-name",
64
+ # hostname: "hey-default-101",
65
+ # last_heartbeat_at: Fri, 26 Jan 2024 20:31:09.652174000 UTC +00:00,
66
+ # configuration: { ... }
67
+ # raw_data: { ... }
68
+ # }
69
+ def workers
70
+ if exposes_workers?
71
+ raise_incompatible_adapter_error_from :workers
72
+ end
73
+ end
74
+
75
+ # Returns a worker represented by a hash as indicated above
76
+ def find_worker(worker_id)
77
+ if exposes_workers?
78
+ raise_incompatible_adapter_error_from :find_worker
79
+ end
80
+ end
81
+
82
+
83
+ # Returns an array with the list of queues. Each queue is represented as a hash
84
+ # with these attributes:
85
+ # {
86
+ # name: "queue_name",
87
+ # size: 1,
88
+ # active: true
89
+ # }
90
+ def queues
91
+ raise_incompatible_adapter_error_from :queue_names
92
+ end
93
+
94
+ def queue_size(queue_name)
95
+ raise_incompatible_adapter_error_from :queue_size
96
+ end
97
+
98
+ def clear_queue(queue_name)
99
+ raise_incompatible_adapter_error_from :clear_queue
100
+ end
101
+
102
+ def pause_queue(queue_name)
103
+ if supports_queue_pausing?
104
+ raise_incompatible_adapter_error_from :pause_queue
105
+ end
106
+ end
107
+
108
+ def resume_queue(queue_name)
109
+ if supports_queue_pausing?
110
+ raise_incompatible_adapter_error_from :resume_queue
111
+ end
112
+ end
113
+
114
+ def queue_paused?(queue_name)
115
+ if supports_queue_pausing?
116
+ raise_incompatible_adapter_error_from :queue_paused?
117
+ end
118
+ end
119
+
120
+ def jobs_count(jobs_relation)
121
+ raise_incompatible_adapter_error_from :jobs_count
122
+ end
123
+
124
+ def fetch_jobs(jobs_relation)
125
+ raise_incompatible_adapter_error_from :fetch_jobs
126
+ end
127
+
128
+ def retry_all_jobs(jobs_relation)
129
+ raise_incompatible_adapter_error_from :retry_all_jobs
130
+ end
131
+
132
+ def retry_job(job, jobs_relation)
133
+ raise_incompatible_adapter_error_from :retry_job
134
+ end
135
+
136
+ def discard_all_jobs(jobs_relation)
137
+ raise_incompatible_adapter_error_from :discard_all_jobs
138
+ end
139
+
140
+ def discard_job(job, jobs_relation)
141
+ raise_incompatible_adapter_error_from :discard_job
142
+ end
143
+
144
+ def dispatch_job(job, jobs_relation)
145
+ raise_incompatible_adapter_error_from :dispatch_job
146
+ end
147
+
148
+ def find_job(job_id, *)
149
+ raise_incompatible_adapter_error_from :find_job
150
+ end
151
+
152
+ private
153
+ def raise_incompatible_adapter_error_from(method_name)
154
+ raise MissionControl::Jobs::Errors::IncompatibleAdapter, "Adapter #{ActiveJob.adapter_name(self)} must implement `#{method_name}`"
155
+ end
156
+ end
@@ -0,0 +1,20 @@
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
+ adapter, cleaner = queue_adapter
15
+
16
+ servers << MissionControl::Jobs::Server.new(name: name.to_s, queue_adapter: adapter,
17
+ backtrace_cleaner: cleaner, application: self)
18
+ end
19
+ end
20
+ 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,24 @@
1
+ # Replaces argument values with [FILTERED] for any keys that match a filter.
2
+ class MissionControl::Jobs::ArgumentsFilter
3
+ FILTERED = "[FILTERED]"
4
+
5
+ def initialize(filter)
6
+ @filter = filter
7
+ end
8
+
9
+ def apply_to(arguments)
10
+ case arguments
11
+ when Array
12
+ arguments.map { |a| apply_to(a) }
13
+ when Hash
14
+ arguments.map do |k, v|
15
+ [ k, filter.include?(k.to_s) ? FILTERED : v ]
16
+ end.to_h
17
+ else
18
+ arguments
19
+ end
20
+ end
21
+
22
+ private
23
+ attr_reader :filter
24
+ end
@@ -0,0 +1,67 @@
1
+ require "rails/command"
2
+
3
+ class MissionControl::Jobs::Authentication < Rails::Command::Base
4
+ def self.configure
5
+ new.configure
6
+ end
7
+
8
+ def configure
9
+ if credentials_accessible?
10
+ if authentication_configured?
11
+ say "HTTP Basic Authentication is already configured for `#{Rails.env}`. You can edit it using `credentials:edit`"
12
+ else
13
+ say "Setting up credentials for HTTP Basic Authentication for `#{Rails.env}` environment."
14
+ say ""
15
+
16
+ username = ask "Enter username: "
17
+ password = SecureRandom.base58(64)
18
+
19
+ store_credentials(username, password)
20
+ say "Username and password stored in Rails encrypted credentials."
21
+ say ""
22
+ say "You can now access Mission Control – Jobs with: "
23
+ say ""
24
+ say " - Username: #{username}"
25
+ say " - password: #{password}"
26
+ say ""
27
+ say "You can also edit these in the future via `credentials:edit`"
28
+ end
29
+ else
30
+ say "Rails credentials haven't been configured or aren't accessible. Configure them following the instructions in `credentials:help`"
31
+ end
32
+ end
33
+
34
+ private
35
+ attr_reader :environment
36
+
37
+ def credentials_accessible?
38
+ credentials.read.present?
39
+ end
40
+
41
+ def authentication_configured?
42
+ %i[ http_basic_auth_user http_basic_auth_password ].any? do |key|
43
+ credentials.dig(:mission_control, key).present?
44
+ end
45
+ end
46
+
47
+ def store_credentials(username, password)
48
+ content = credentials.read + "\n" + http_authentication_entry(username, password) + "\n"
49
+ credentials.write(content)
50
+ end
51
+
52
+ def credentials
53
+ @credentials ||= Rails.application.encrypted(config.content_path, key_path: config.key_path)
54
+ end
55
+
56
+ def config
57
+ Rails.application.config.credentials
58
+ end
59
+
60
+ def http_authentication_entry(username, password)
61
+ <<~ENTRY
62
+ mission_control:
63
+ http_basic_auth_user: #{username}
64
+ http_basic_auth_password: #{password}
65
+ ENTRY
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ require "irb/command"
2
+
3
+ module MissionControl::Jobs::Console
4
+ class ConnectTo < IRB::Command::Base
5
+ category "Mission control jobs"
6
+ description "Connect to a job server"
7
+
8
+ def execute(server_locator)
9
+ server = MissionControl::Jobs::Server.from_global_id(server_locator)
10
+ MissionControl::Jobs::Current.server = server
11
+
12
+ puts "Connected to #{server_locator}"
13
+ end
14
+ end
15
+ 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,23 @@
1
+ require "irb/command"
2
+
3
+ module MissionControl::Jobs::Console
4
+ class JobsHelp < IRB::Command::Base
5
+ category "Mission control jobs"
6
+ description "Show help for managing jobs"
7
+
8
+ def execute(*)
9
+ puts "You are currently connected to #{MissionControl::Jobs::Current.server}" if MissionControl::Jobs::Current.server
10
+
11
+ puts "You can connect to a job server with"
12
+ puts %( connect_to <app_id>:<server_id>\n\n)
13
+
14
+ puts "Available job servers:\n"
15
+
16
+ MissionControl::Jobs.applications.each do |application|
17
+ application.servers.each do |server|
18
+ puts " * #{server.to_global_id}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end